<!DOCTYPE html>
<html>
  <!-- meta/link... -->
  



<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
  <!-- Global site tag (gtag.js) - Google Analytics -->


  <title>DDD四层架构解析 | 老顾的个人博客网站</title>

  <link rel="icon" type="image/x-icon, image/vnd.microsoft.icon" href="/my-blog/favicon.ico">
  <link rel="stylesheet" href="https://at.alicdn.com/t/font_1911880_c1nvbyezg17.css">
  <link href="https://unpkg.com/@fortawesome/fontawesome-free/css/all.min.css" rel="stylesheet">
  <link href="/my-blog/js/swiper/swiper@5.4.1.min.css" rel="stylesheet">
  
  
  
  
<link rel="stylesheet" href="/my-blog/css/animate.min.css">

  
<link rel="stylesheet" href="/my-blog/css/style.css">

  
  
    
<link rel="stylesheet" href="/my-blog/js/fancybox/jquery.fancybox.min.css">

  
  
    
<link rel="stylesheet" href="/my-blog/js/shareJs/share.min.css">

  
  <style>
        @media (max-width: 992px) {
            #waifu {
                display: none;
            }
        }
    </style>
    <script src="//cdn.bootcss.com/pace/1.0.2/pace.min.js"></script>
    <link href="//cdn.bootcss.com/pace/1.0.2/themes/pink/pace-theme-flash.css" rel="stylesheet">

    
        <script src="/my-blog/js/valine/index.js"></script>
    

    <!-- import link -->
    
        
            
        
            
        
    
    <!-- import script -->
    
        
            
        
            
        
    

<meta name="generator" content="Hexo 6.3.0"></head>

  
  <!-- 依赖于jquery和vue -->
  
    
<script src="https://unpkg.com/jquery@3.5.1/dist/jquery.min.js"></script>

  

  
    
<script src="https://unpkg.com/vue@2.6.11/dist/vue.min.js"></script>

  
  
  <!-- 预加载动画 -->
  <!-- 页面预加载动画 -->

  
    <div class="preloader_3" id="loader">
  <div class="bar">
    <div class="ball"></div>
  </div>
</div>

  
<script>
  var endLoading = function () {
    document.body.style.overflow = 'auto';
    document.getElementById('loader').classList.add("loading");
  }
  window.addEventListener('DOMContentLoaded',endLoading);
</script>

  <body>
    <!-- 判断是否为暗黑风格 -->
    <!-- 判断是否为黑夜模式 -->
<script>
  let isDark = JSON.parse(localStorage.getItem('dark')) || JSON.parse('false');

  if (isDark) {
    $(document.body).addClass('darkModel');
  }
</script>

    <!-- 需要在上面加载的js -->
    <script>
  function loadScript(src, cb) {
    return new Promise(resolve => {
      setTimeout(function () {
        var HEAD = document.getElementsByTagName("head")[0] || document.documentElement;
        var script = document.createElement("script");
        script.setAttribute("type", "text/javascript");
        if (cb) {
          if (JSON.stringify(cb)) {
            for (let p in cb) {
              if (p == "onload") {
                script[p] = () => {
                  cb[p]()
                  resolve()
                }
              } else {
                script[p] = cb[p]
                script.onload = resolve
              }
            }
          } else {
            script.onload = () => {
              cb()
              resolve()
            };
          }
        } else {
          script.onload = resolve
        }
        script.setAttribute("src", src);
        HEAD.appendChild(script);
      });
    });
  }

  //https://github.com/filamentgroup/loadCSS
  var loadCSS = function (src) {
    return new Promise(resolve => {
      setTimeout(function () {
        var link = document.createElement('link');
        link.rel = "stylesheet";
        link.href = src;
        link.onload = resolve;
        document.getElementsByTagName("head")[0].appendChild(link);
      });
    });
  };

</script> 

<!-- 轮播图所需要的js -->
<script src="/my-blog/js/swiper/swiper.min.js"></script>
<script src="/my-blog/js/swiper/vue-awesome-swiper.js"></script>
<script src="/my-blog/js/swiper/swiper.animate1.0.3.min.js"></script>

<script type="text/javascript">
  Vue.use(window.VueAwesomeSwiper)
</script>


  <script src="/my-blog/js/vue-typed-js/index.js"></script>


<!-- 首页的公告滚动插件的js需要重新加载 -->
<script src="/my-blog/js/vue-seamless-scroll/index.js"></script>

<!-- 打字机效果js -->
<script src="https://unpkg.com/typed.js@2.0.11"></script>


    <div id="safearea">
      <main class="main" id="pjax-container">
        <!-- 头部导航 -->
        
<header class="header  " 
  id="navHeader"
  style="position: fixed;
  left: 0; top: 0; z-index: 10;width: 100%;"
>
  <div class="header-content">
    <div class="bars">
      <div id="appDrawer" class="sidebar-image">
  <div class="drawer-box-icon">
    <i class="fas fa-bars" aria-hidden="true" @click="showDialogDrawer"></i>
  </div>
  
  <transition name="fade">
    <div class="drawer-box_mask" v-cloak style="display: none;" v-show="visible" @click.self="cancelDialogDrawer">
    </div>
  </transition>
  <div class="drawer-box" :class="{'active': visible}">
    <div class="drawer-box-head bg-color">
      <img class="drawer-box-head_logo lazyload placeholder" src="/my-blog/medias/logo.png" class="lazyload placeholder" data-srcset="/my-blog/medias/logo.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="logo">
      <h3 class="drawer-box-head_title">老顾的个人博客网站</h3>
      <h5 class="drawer-box-head_desc">芝兰生于幽谷，不已无人而不芳</h5>
    </div>
    
    <div class="drawer-box-content">
      <ul class="drawer-box-content_menu">
        
          
            <li class="drawer-box-content_item" style="position: relative;">
              
                <a href="/my-blog/" class="drawer-menu-item-link">
                  
                    <i class="fas fa-home" aria-hidden="true"></i>
                  
                  <span class="name">首页</span>
                </a>
              
            </li>
          
            <li class="drawer-box-content_item" style="position: relative;">
              
                <a href="/my-blog/archives" class="drawer-menu-item-link">
                  
                    <i class="fas fa-archive" aria-hidden="true"></i>
                  
                  <span class="name">归档</span>
                </a>
              
            </li>
          
            <li class="drawer-box-content_item" style="position: relative;">
              
                <a href="/my-blog/tags" class="drawer-menu-item-link">
                  
                    <i class="fas fa-tags" aria-hidden="true"></i>
                  
                  <span class="name">标签</span>
                </a>
              
            </li>
          
            <li class="drawer-box-content_item" style="position: relative;">
              
                <a href="/my-blog/categories" class="drawer-menu-item-link">
                  
                    <i class="fas fa-bookmark" aria-hidden="true"></i>
                  
                  <span class="name">分类</span>
                </a>
              
            </li>
          
            <li class="drawer-box-content_item" style="position: relative;">
              
                <a href="/my-blog/about" class="drawer-menu-item-link">
                  
                  <span class="name">关于</span>
                </a>
              
            </li>
          
            <li class="drawer-box-content_item" style="position: relative;">
              
                <a href="javascript:;" class="drawer-menu-item-link has-children" @click="openOrCloseMenu(5)">
                  <span>
                    
                      <i class="fas fa-link"></i>
                    
                    <span class="name">更多</span>
                  </span>
                  <i class="fas fa-chevron-left arrow " :class="{'icon-rotate': isOpen(5)}" aria-hidden="true"></i>
                </a>
                <ul class="drawer-sub-menu" v-if="isOpen(5)">
                  
                  <li>
                    <a href="/my-blog/gallery">
                      
                      <i class="fas fa-music" style="margin-top: -20px;"></i>
                      
                      <span>图库</span>
                    </a>
                  </li>
                  
                  <li>
                    <a target="_blank" rel="noopener" href="http://baidu.com">
                      
                      <i class="fas fa-film" style="margin-top: -20px;"></i>
                      
                      <span>百度</span>
                    </a>
                  </li>
                  
                </ul>
              
            </li>
          
        
        
          <li class="drawer-box-content_item">
            <a target="_blank" rel="noopener" href="https://github.com/Engineer-Gu">
              <i class="fas fa-github" aria-hidden="true"></i>
              <span>Github</span>
            </a>
          </li>
        
      </ul>
    </div>
  </div>
</div>

<script>
  var body = document.body || document.documentElement || window;
  var vm = new Vue({
    el: '#appDrawer',
    data: {
      visible: false,
      top: 0,
      openArr: [],
    },
    computed: {
    },
    mounted() {
    },
    methods: {
      isOpen(index) {
        if (this.openArr.includes(index)) {
          return true;
        } else {
          return false;
        }
      },
      openOrCloseMenu(curIndex) {
        const index = this.openArr.indexOf(curIndex);
        if (index !== -1) {
          this.openArr.splice(index, 1);
        } else {
          this.openArr.push(curIndex);
        }
      },
      showDialogDrawer() {
        this.visible = true;
        // 防止页面滚动，只能让弹框滚动
        this.top = $(document).scrollTop()
        body.style.cssText = 'width: 100%; height: 100%;overflow: hidden;';
      },
      cancelDialogDrawer() {
        this.visible = false;
        body.removeAttribute('style');
        $(document).scrollTop(this.top)
      }
    },
    created() {}
  })
</script>

    </div>
    <div class="blog-title" id="author-avatar">
      
        <div class="avatar">
          <img src="/my-blog/medias/logo.png" class="lazyload placeholder" data-srcset="/my-blog/medias/logo.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="logo">
        </div>
      
      <a href="/my-blog/" class="logo">老顾的个人博客网站</a>
    </div>
    <nav class="navbar">
      <ul class="menu">
        
          
            <li class="menu-item" style="position: relative;">
              
                <a href="/my-blog/" class="menu-item-link" title="首页">
                  
                    <i class="fas fa-home" aria-hidden="true"></i>
                  
                  <span class="name">首页</span>
                </a>
              
            </li>
          
            <li class="menu-item" style="position: relative;">
              
                <a href="/my-blog/archives" class="menu-item-link" title="归档">
                  
                    <i class="fas fa-archive" aria-hidden="true"></i>
                  
                  <span class="name">归档</span>
                </a>
              
            </li>
          
            <li class="menu-item" style="position: relative;">
              
                <a href="/my-blog/tags" class="menu-item-link" title="标签">
                  
                    <i class="fas fa-tags" aria-hidden="true"></i>
                  
                  <span class="name">标签</span>
                </a>
              
            </li>
          
            <li class="menu-item" style="position: relative;">
              
                <a href="/my-blog/categories" class="menu-item-link" title="分类">
                  
                    <i class="fas fa-bookmark" aria-hidden="true"></i>
                  
                  <span class="name">分类</span>
                </a>
              
            </li>
          
            <li class="menu-item" style="position: relative;">
              
                <a href="/my-blog/about" class="menu-item-link" title="关于">
                  
                  <span class="name">关于</span>
                </a>
              
            </li>
          
            <li class="menu-item" style="position: relative;">
              
                <a href="javascript:;" class="menu-item-link" title="更多">
                  
                    <i class="fas fa-link"></i>
                  
                  <span class="name">更多</span>
                  <i class="fas fa-chevron-down arrow" aria-hidden="true"></i>
                </a>
                <ul class="sub-menu">
                  
                  <li>
                    <a href="/my-blog/gallery">
                      
                      <i class="fas fa-music" style="margin-top: -20px;"></i>
                      
                      <span>图库</span>
                    </a>
                  </li>
                  
                  <li>
                    <a target="_blank" rel="noopener" href="http://baidu.com">
                      
                      <i class="fas fa-film" style="margin-top: -20px;"></i>
                      
                      <span>百度</span>
                    </a>
                  </li>
                  
                </ul>
              
            </li>
          
        
      </ul>
      
      
        <div id="appSearch">
  <div class="search"  @click="showDialog()"><i class="fas fa-search" aria-hidden="true"></i></div>
  <transition name="fade">
    <div class="message-box_wrapper" style="display: none;" v-cloak v-show="dialogVisible" @click.self="cancelDialogVisible()">
      <div class="message-box animated bounceInDown">
        <h2>
          <span>
            <i class="fas fa-search" aria-hidden="true"></i>
            <span class="title">本地搜索</span>
          </span>
          <i class="fas fa-times close" pointer style="float:right;" aria-hidden="true" @click.self="cancelDialogVisible()"></i>
        </h2>
        <form class="site-search-form">
          <input type="text"
            placeholder="请输入关键字"
            id="local-search-input" 
            @click="getSearchFile()"
            class="st-search-input"
            v-model="searchInput"
          />
        </form>
        <div class="result-wrapper">
          <div id="local-search-result" class="local-search-result-cls"></div>
        </div>
      </div>
    </div>
  </transition>
</div>
<script src="/my-blog/js/local_search.js"></script>
<script>
  var body = document.body || document.documentElement || window;
  var vm = new Vue({
    el: '#appSearch',
    data: {
      dialogVisible: false,
      searchInput: '',
      top: 0,
    },
    computed: {
    },
    mounted() {
      window.addEventListener('pjax:complete', () => {
        this.cancelDialogVisible();
      })
    },
    methods: {
      showDialog() {
        this.dialogVisible = true;
        // 防止页面滚动，只能让弹框滚动
        this.top = $(document).scrollTop()
        body.style.cssText = 'overflow: hidden;';
      },
      getSearchFile() {
        if (!this.searchInput) {
          getSearchFile("/my-blog/search.xml");
        }
      },
      cancelDialogVisible() {
        this.dialogVisible = false;
        body.removeAttribute('style');
        $(document).scrollTop(this.top)
      },
    },
    created() {}
  })
</script>
<!-- 解决刷新页面闪烁问题，可以在元素上添加display: none, 或者用vue.extend方法，详情：https://blog.csdn.net/qq_31393401/article/details/81017912 -->
<!-- 下面是搜索基本写法 -->
<!-- <script type="text/javascript" id="local.search.active">
  var inputArea = document.querySelector("#local-search-input");
  inputArea.onclick   = function(){ getSearchFile(); this.onclick = null }
  inputArea.onkeydown = function(){ if(event.keyCode == 13) return false }
</script> -->

      

    </nav>
  </div>
  
    <a target="_blank" rel="noopener" href="https://github.com/Engineer-Gu" class="github-corner color-primary" aria-label="View source on GitHub"><svg width="60" height="60" viewBox="0 0 250 250" style="fill:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
  
  
    <div id="he-plugin-simple"></div>
    <script>
      WIDGET = {
        CONFIG: {
          "modules": "012",
          "background": 5,
          "tmpColor": "4A4A4A",
          "tmpSize": 16,
          "cityColor": "4A4A4A",
          "citySize": 16,
          "aqiSize": 16,
          "weatherIconSize": 24,
          "alertIconSize": 18,
          "padding": "10px 10px 10px 10px",
          "shadow": "1",
          "language": "auto",
          "borderRadius": 5,
          "fixed": "false",
          "vertical": "middle",
          "horizontal": "center",
          "key": "2784dd3fcb1e4f0f9a9b579bf69641f2"
        }
      }
    </script>
    <script src="https://widget.qweather.net/simple/static/js/he-simple-common.js?v=2.0"></script> 
    
</header>
        <!-- 内容区域 -->
        
 <!-- prismjs 代码高亮 -->
 


<div class="bg-dark-floor" style="position: fixed;left: 0;top: 0;width: 100%;height: 100%;z-index: -1;"></div>


  <!-- 文章详情页顶部图片和标题 -->




<div class="post-detail-header" id="thumbnail_canvas" style="background-repeat: no-repeat; background-size: cover; 
  background-position: center center;position: relative;background-image:url('https://pic1.zhimg.com/80/v2-03a22891ccba9bccf6424dfd7cbf4be7_1440w.webp')">
  <div class="post-detail-header-mask"></div>
  <canvas id="header_canvas"style="position:absolute;bottom:0;pointer-events:none;"></canvas>
  
  <div class="post-detail-header_info-box">
    <div class="title-box">
      <span class="title">
        DDD四层架构解析
      </span>
    </div>
    
    
      
        <span class="post-detail-header_date">
          <i class="fas fa-calendar"></i> 发表于：2023-06-29 |
        </span>
      

      

      
        <div class="post-detail-header_wordcount">
          <span class="totalcount">
            <i class="fas fa-file-text-o"></i> 字数统计: 11.8k |
          </span>
  
          <span class="min2read">
            <i class="fas fa-clock"></i> 阅读时长: 46分钟 |
          </span>
  
          
            <span class="reading">
              <i class="fas fa-eye"></i> 阅读量：<span id="busuanzi_value_page_pv"></span>
            </span>
          
        </div>
      
    
  </div>
  
  
    <script src="/my-blog/js/bubble/bubble.js"></script>
  
</div>





<div class="post-detail-content post-row" 
  style="padding-top: 0px;">
  <div class="main-content">
    <article class="post post-detail">
      <div class="post-content">
        <p>DDD，也就是Domain-Driven Design：基于领域的设计</p>
<p>什么是领域呢？</p>
<p>可以暂时将其理解为”业务问题的范畴”，领域可大可小，对应着大小业务问题的边界，简单来说，领域驱动设计就是将业务上要做的一件大事，通过推演和抽象，拆分成多个内聚的领域。</p>
<p>这跟模块化和微服务很相似，无论是模块化还是微服务还是其它各种方法论和工程实践，只能认为它们都是为了达成降低软件开发维护复杂度这个目的去遵循解耦的原则，它们属于软件开发中不同层面的实现方式。</p>
<h2 id="DP-x2F-VO"><a href="#DP-x2F-VO" class="headerlink" title="DP&#x2F;VO"></a>DP&#x2F;VO</h2><h3 id="一个简单的业务案例"><a href="#一个简单的业务案例" class="headerlink" title="一个简单的业务案例"></a>一个简单的业务案例</h3><blockquote>
<p>假设现在在做一个简单的数据统计系统，地推员输入客户的姓名和手机号，根据客户手机号的归属地和所属运营商将客户群体分组，分配给相应销售组，由销售组跟进后续的业务</p>
</blockquote>
<p>简单分析一下：根据这段需求，我们需要提供一个注册服务，这个注册服务的入参应该是客户的姓名的手机号，服务内部根据这个手机号查对应归属地编号和运营商编号，再根据这两个值获取分组号，最后将客户的姓名、手机号、分组号封装成一个对象存入数据表。</p>
<p>简单版本代码实现：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 定义User类，一个注册接口的具体实现类</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> &#123;</span><br><span class="line">    Long userId;</span><br><span class="line">    String name;</span><br><span class="line">    String phone;</span><br><span class="line">    Long repId;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SalesRepRepository salesRepRepo;</span><br><span class="line">    <span class="keyword">private</span> UserRepository userRepo;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 这里的redister方法存在两个类型为String的参数，第一个为用户名，第二个为手机号</span></span><br><span class="line">    <span class="comment">// 方法内部一开始对其合法性进行了校验，当这段代码被编译后，方法只会保留参数类型，而不会保留参数名</span></span><br><span class="line">    <span class="comment">// 如果这段代码存在于近端包内被其它项目集成时，其他程序员并不了解这个方法内部逻辑</span></span><br><span class="line">    <span class="comment">// 或者说仅仅是因为失误，他很有可能颠倒参数的顺序，这种错误虽然低级，但人人也不能完全避免</span></span><br><span class="line">    <span class="comment">// 并且代码在编译期是无法检查出来的。此外，未来系统开始支持通过用户名和身份证号进行注册</span></span><br><span class="line">    <span class="comment">// 身份证号也是一个String，假如未来又要通过手机号又要通过身份证号注册呢？这样一来，接口修改会很频繁</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">register</span><span class="params">(String name, String phone)</span></span><br><span class="line">        <span class="keyword">throws</span> ValidationException &#123;</span><br><span class="line">        <span class="comment">// 注册方法中先对参数进行校验，如果存在多个类型的方法，那么每个方法都要在开头进行校验</span></span><br><span class="line">        <span class="comment">// 这里一定会存在大量的重复代码，而且一旦某种类型的参数校验逻辑需要修改</span></span><br><span class="line">        <span class="comment">// 那么每个地方还要一一检查，这显然不符合&quot;开闭原则&quot;</span></span><br><span class="line">        <span class="comment">// 有些人可能会很自然想到将这些校验逻辑封装进入某个工具类，这样就能进行一定程度的复用</span></span><br><span class="line">        <span class="comment">// 这样虽然解决了一些问题，但不算是一种最佳实践</span></span><br><span class="line">        <span class="comment">// 因为还存在缺点：</span></span><br><span class="line">        <span class="comment">// 第一，业务方法还是需要主动调用工具类来进行校验，如果校验失败就要抛出异常</span></span><br><span class="line">        <span class="comment">// 这样在业务方法中把参数异常和业务逻辑异常混合起来不太合理</span></span><br><span class="line">        <span class="comment">// 第二：一旦参数越来越多，那么工具类中的校验逻辑会随之不断膨胀，后续也不太好维护</span></span><br><span class="line">        <span class="keyword">if</span> (name == <span class="literal">null</span> || name.length() == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ValidationException</span>(<span class="string">&quot;name&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (phone == <span class="literal">null</span> || !isValidPhoneNumber(phone)) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ValidationException</span>(<span class="string">&quot;phone&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 获取手机号归属地编号和运营商编号 然后通过编号找到区域内的SalesRep</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">areaCode</span> <span class="operator">=</span> getAreaCode(phone);</span><br><span class="line">        <span class="type">String</span> <span class="variable">operatorCode</span> <span class="operator">=</span> getOperatorCode(phone);</span><br><span class="line">        <span class="type">SalesRep</span> <span class="variable">rep</span> <span class="operator">=</span> salesRepRepo.findRep(areaCode, operatorCode);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 最后创建用户，落盘，然后返回</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">        user.name = name;</span><br><span class="line">        user.phone = phone;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (rep != <span class="literal">null</span>) &#123;</span><br><span class="line">            user.repId = rep.repId;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> userRepo.save(user);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">isValidPhoneNumber</span><span class="params">(String phone)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">&quot;^0[1-9]&#123;2,3&#125;-?\\d&#123;8&#125;$&quot;</span>;</span><br><span class="line">        <span class="keyword">return</span> phone.matches(pattern);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">getAreaCode</span><span class="params">(String phone)</span> &#123;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">getOperatorCode</span><span class="params">(String phone)</span> &#123;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>如果是一个小工程或者说迭代低频，甚至短期内有可能下线的系统，这样写一点问题都没有，甚至可以称得上是又快又好，但如果将这样的代码写在一个迭代频繁的大工程内，其实存在一些隐患</p>
<p>接口定义修改目标：</p>
<ul>
<li>语义明确无歧义</li>
<li>拓展性强一些</li>
<li>最好还能带有一定自检性</li>
</ul>
<p>参数校验修改目标：</p>
<ul>
<li>提高校验逻辑复用性</li>
<li>参数校验异常与业务逻辑异常解耦</li>
</ul>
<p>审视点：接口语义与参数校验</p>
<p>修改目标：</p>
<ul>
<li>接口语义明确，可拓展性强，最好带有自检性</li>
<li>参数校验逻辑复用，内聚</li>
<li>参数校验异常和业务逻辑异常解耦</li>
</ul>
<h3 id="DP初体验"><a href="#DP初体验" class="headerlink" title="DP初体验"></a>DP初体验</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> User <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PhoneNumber</span> &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">final</span> String number;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">&quot;^0?[1-9]&#123;2,3&#125;-?\\d&#123;8&#125;$&quot;</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">public</span> String <span class="title function_">getNumber</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> number;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 仅存在含参构造器</span></span><br><span class="line">        <span class="keyword">public</span> <span class="title function_">PhoneNumber</span><span class="params">(String number)</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (number == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ValidationException</span>(<span class="string">&quot;number不能为空&quot;</span>);</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (isValid(number)) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ValidationException</span>(<span class="string">&quot;number格式错误&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="built_in">this</span>.number = number;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">isValid</span><span class="params">(String number)</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> number.matches(pattern);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>

<h3 id="示例改造"><a href="#示例改造" class="headerlink" title="示例改造"></a>示例改造</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> &#123;</span><br><span class="line">    Long userId;</span><br><span class="line">    String name;</span><br><span class="line">    PhoneNumber phone;</span><br><span class="line">    Long repId;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SalesRepRepository salesRepRepo;</span><br><span class="line">    <span class="keyword">private</span> UserRepository userRepo;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 获取手机号归属地编号和运营商编号，然后通过编号找到区域内的SalesRep</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">areaCode</span> <span class="operator">=</span> getAreaCode(phone);</span><br><span class="line">        <span class="type">String</span> <span class="variable">operatorCode</span> <span class="operator">=</span> getOperatorCode(phone);</span><br><span class="line">        <span class="comment">// 在register方法里写这些逻辑仅仅是为了适配findRep这个接口来对原始的参数进行处理拼接</span></span><br><span class="line">        <span class="comment">// 就像是拿胶水进行缝缝补补，这被形象的成为&quot;胶水逻辑&quot;</span></span><br><span class="line">        <span class="type">SalesRep</span> <span class="variable">rep</span> <span class="operator">=</span> salesRepRepo.findRep(areaCode, operatorCode);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 最后创建用户，落盘，然后返回</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">        user.name = name;</span><br><span class="line">        user.phone = phone;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (rep != <span class="literal">null</span>) &#123;</span><br><span class="line">            user.repId = rep.repId;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> userRepo.save(user);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">getAreaCode</span><span class="params">(PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">getOperatorCode</span><span class="params">(PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p> 把手机号归属地编号和运营商编号放在”注册”这个业务域里其实并不合适，什么逻辑应该归属于哪个业务域，这就是对”领域”的理解，就像如何对微服务进行边界限定一样，不同的理解角度会产生不同的领域模型划分，回到代码，获取归属地编号和获取运营商编号这些逻辑并不应该属于注册这个领域</p>
<p>如何改造胶水逻辑才合理呢？</p>
<p>有两个思路：</p>
<ul>
<li>第一种方式：改造这个接口的入参</li>
<li>第二种方式：假如这个findRep是外部参数，没有办法修改入参类型，”获取手机号归属地编码”和”获取手机号运营商编码”这两个行为都是获取手机号相关的熟悉，应该内聚在手机号这个类型中，这在抽象上也是合理的</li>
</ul>
<h3 id="DP优化"><a href="#DP优化" class="headerlink" title="DP优化"></a>DP优化</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PhoneNumber</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String number;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">pattern</span> <span class="operator">=</span> <span class="string">&quot;^0?[1-9]&#123;2,3&#125;-?\\d&#123;8&#125;$&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getNumber</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> number;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 仅存在含参构造器</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">PhoneNumber</span><span class="params">(String number)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (number == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ValidationException</span>(<span class="string">&quot;number不能为空&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (isValid(number)) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ValidationException</span>(<span class="string">&quot;number格式错误&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">this</span>.number = number;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">isValid</span><span class="params">(String number)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> number.matches(pattern);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getAreaCode</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getOperatorCode</span><span class="params">(PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> &#123;</span><br><span class="line">    Long userId;</span><br><span class="line">    String name;</span><br><span class="line">    PhoneNumber phone;</span><br><span class="line">    Long repId;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SalesRepRepository salesRepRepo;</span><br><span class="line">    <span class="keyword">private</span> UserRepository userRepo;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 获取用户信息</span></span><br><span class="line">        <span class="type">SalesRep</span> <span class="variable">rep</span> <span class="operator">=</span> salesRepRepo.findRep(phone.getAreaCode(), phone.getOperatorCode());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 存储用户信息</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">        user.name = name;</span><br><span class="line">        user.phone = phone;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (rep != <span class="literal">null</span>) &#123;</span><br><span class="line">            user.repId = rep.repId;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> userRepo.save(user);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>思考题：</p>
<p>目前获取归属地编号和获取运营商编号方法只是简单字符串处理逻辑，没有外部依赖，内聚在PhoneNumber类中恰到好处，假设现在需要对手机号注册时的身份信息和入参中的用户名进行一致性校验（获取手机号的身份信息是一个外部依赖项）如果在PhoneNumber内部实现，让PhoneNumber内依赖其它查询服务似乎是不合适的，那么，这部分逻辑应该怎么去抽象？</p>
<h2 id="Entity、DomainService、Repository"><a href="#Entity、DomainService、Repository" class="headerlink" title="Entity、DomainService、Repository"></a>Entity、DomainService、Repository</h2><h3 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h3><p>假设现在在做一个简单的数据统计系统，市场专员输入客户的姓名和手机号，根据客户手机号的归属地和所属运营商，将客户群体分组，分配给相应销售专员，由销售专员跟进后续的业务。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SalesRepRepository salesRepRepo;</span><br><span class="line">    <span class="keyword">private</span> UserRepository userRepo;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> UserDO <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 获取用户信息</span></span><br><span class="line">        <span class="type">SalesRep</span> <span class="variable">rep</span> <span class="operator">=</span> salesRepRepo.findRep(phone.getAreaCode(), phone.getOperatorCode());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 存储用户信息</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">        user.name = name;</span><br><span class="line">        user.phone = phone;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (rep != <span class="literal">null</span>) &#123;</span><br><span class="line">            user.repId = rep.repId;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> userRepo.save(user);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="回退到无Repository版本"><a href="#回退到无Repository版本" class="headerlink" title="回退到无Repository版本"></a>回退到无Repository版本</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SalesRepMapper salesRepDAO;</span><br><span class="line">    <span class="keyword">private</span> UserMapper userDAO;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> UserDO <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 获取用户信息</span></span><br><span class="line">        <span class="type">SalesRepDO</span> <span class="variable">repDO</span> <span class="operator">=</span> salesRepDAO.select(phone.getAreaCode(), phone.getOperatorCode());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 存储用户信息</span></span><br><span class="line">        <span class="type">UserDO</span> <span class="variable">userDO</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UserDO</span>();</span><br><span class="line">        userDO.name = name;</span><br><span class="line">        userDO.phone = phone;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (repDO != <span class="literal">null</span>) &#123;</span><br><span class="line">            userDO.repId = repDO.repId;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> userDAO.insert(userDO);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="增加点功能"><a href="#增加点功能" class="headerlink" title="增加点功能"></a>增加点功能</h3><ol>
<li>需要对手机号进行实名校验，实名信息通过调用外部服务获得。（假设目前由中国电信提供该服务）</li>
<li>根据外部服务返回的实名信息，按照一定逻辑计算出用户标签，记录在用户账号中。</li>
<li>根据用户标签为该用户开通相应等级的新客福利。</li>
</ol>
<h3 id="需求梳理"><a href="#需求梳理" class="headerlink" title="需求梳理"></a>需求梳理</h3><ol>
<li>业务方法的入参是客户姓名和手机号，首先使用手机号去调用外部服务查询实名信息，校对是否和入参中的姓名一致，如果一致，则通过。</li>
<li>然后，然后根据得到的实名信息，按照一定逻辑计算得出该用户的标签，该标签将作为用户的一个属性。</li>
<li>接着，根据手机号的归属地和所属运营商，查询得到关联的销售组信息，该销售组ID将作为用户的一个属性。</li>
<li>此时，根据用户信息，构建用户对象和福利对象，并查询风控是否通过。</li>
<li>若通过，用户失去新客身份，且可以查询到福利信息，数据落库。<br>若不通过，用户保持新客身份，但查询不到福利信息，数据落库。</li>
<li>上述逻辑默认在同一个事务中处理。</li>
</ol>
<p><img src="https://pic4.zhimg.com/80/v2-acf253c98c9b19e18a1c662e5bef9cb3_720w.webp" class="lazyload placeholder" data-srcset="https://pic4.zhimg.com/80/v2-acf253c98c9b19e18a1c662e5bef9cb3_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<h3 id="平平无奇的写法"><a href="#平平无奇的写法" class="headerlink" title="平平无奇的写法"></a>平平无奇的写法</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SalesRepMapper salesRepDAO;</span><br><span class="line">    <span class="keyword">private</span> UserMapper userDAO;</span><br><span class="line">    <span class="keyword">private</span> RewardMapper rewardDAO;</span><br><span class="line">    <span class="keyword">private</span> TelecomRealnameService telecomService;</span><br><span class="line">    <span class="keyword">private</span> RiskControlService riskControlService;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 外部依赖：RPC服务</span></span><br><span class="line">    <span class="keyword">public</span> UserDO <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="comment">// 参数合法性校验已在PhoneNumber中处理</span></span><br><span class="line">        <span class="comment">// 参数一致性校验</span></span><br><span class="line">        <span class="type">TelecomInfoDTO</span> <span class="variable">rnInfoDTO</span> <span class="operator">=</span> telecomService.getRealnameInfo(phone.getNumber());</span><br><span class="line">        <span class="keyword">if</span> (!name.equals(rnInfoDTO.getName())) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">InvalidRealnameException</span>();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 计算用户标签</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">label</span> <span class="operator">=</span> getLabel(rnInfoDTO);</span><br><span class="line">        <span class="comment">// 计算销售组</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">salesRepId</span> <span class="operator">=</span> getSalesRepId(phone);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 构造User对象和Reward对象</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">idCard</span> <span class="operator">=</span> rnInfoDTO.getIdCard();</span><br><span class="line">        <span class="comment">// 外部依赖：数据库schema</span></span><br><span class="line">        <span class="type">UserDO</span> <span class="variable">userDO</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UserDO</span>(idCard, name, phone.getNumber(), label, salesRepId);</span><br><span class="line">        <span class="type">RewardDO</span> <span class="variable">rewardDO</span> <span class="operator">=</span> RewardDO(idCard, label);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 检查风控 外部依赖：RPC服务</span></span><br><span class="line">        <span class="keyword">if</span>(!riskControlService.check(idCard, label)) &#123;</span><br><span class="line">            <span class="comment">// 外部依赖：数据库schema</span></span><br><span class="line">            userDO.setNew(<span class="literal">true</span>);</span><br><span class="line">            rewardDO.setAvailable(<span class="literal">false</span>);</span><br><span class="line">        &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">            userDO.setNew(<span class="literal">false</span>);</span><br><span class="line">            rewardDO.setAvailable(<span class="literal">true</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 存储信息 外部依赖：ORM框架</span></span><br><span class="line">        rewardDAO.insert(rewardDO);</span><br><span class="line">        <span class="keyword">return</span> userDAO.insert(userDO);</span><br><span class="line">    &#125;</span><br><span class="line">	</span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">getLabel</span><span class="params">(TelecomInfoDTO dto)</span> &#123;</span><br><span class="line">        <span class="comment">// 本地逻辑处理</span></span><br><span class="line">    &#125;</span><br><span class="line">	<span class="comment">// 外部依赖：ORM框架</span></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">getSalesRepId</span><span class="params">(PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="type">SalesRepDO</span> <span class="variable">repDO</span> <span class="operator">=</span> salesRepDAO.select(phone.getAreaCode(), phone.getOperatorCode());</span><br><span class="line">        <span class="keyword">if</span> (repDO != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> repDO.getRepId();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>外部依赖</p>
<ul>
<li>数据库schema<ul>
<li>主要操作就是从数据表读出一些信息，然后增加一些信息并写入，这里面的业务代码强依赖了数据库schema，也就是DO类，如果数据表的字段产生变动，对应的DO类也就会进行变动，如果业务逻辑复杂几倍，并且业务逻辑产生变化，那么这些代码会被改的面目全非，而且会破坏原有的正常功能产生bug</li>
<li>面向数据表–&gt;建模–&gt;面向对象，拒绝做CRUD程序员</li>
</ul>
</li>
<li>ORM框架<ul>
<li>上面的代码使用了mybatis框架，使用Mapper来对DAO对象来构建和指向SQL，如果系统需要升级ORM框架，假如框架没有向下兼容，API产生变化，甚至因为某些安全问题系统需要替换掉整个ORM框架，那么业务代码也要随之进行大量的变动，这是不合理且存在风险的</li>
<li>代码拥有良好的抽象，程序员才能以逸待劳</li>
</ul>
</li>
<li>RPC服务<ul>
<li>上面代码使用了中国电信提供的手机号实名信息查询服务，并且强依赖在业务逻辑中，假设中国电信提供的该接口入参和返参都进行了变动或者以后不再和电信合作，转而去使用联通的服务，那么业务逻辑代码也会进行相应的修改</li>
</ul>
</li>
</ul>
<p>由<strong>外部依赖</strong>的变化所导致的内部系统的改造程序，我们可以狭义地理解为系统的<strong>可维护性</strong></p>
<p>造成上面代码耦合程度非常高就是因为上面的代码是在面向具体实现编程，所以，思路就是将其改造成面向抽象接口编程，领域驱动将会作为一种指导思想来辅助我们设计</p>
<p>什么是面向抽象接口编程？</p>
<p>抽象接口本质是一种中间协议，依赖方和被依赖方都只要对该协议负责，接口将软件分层隔离，在这种隔离下任何一方的变动都将被控制在当前范围内</p>
<h3 id="抽象RPC调用"><a href="#抽象RPC调用" class="headerlink" title="抽象RPC调用"></a>抽象RPC调用</h3><p>1、使用RealnameService替代了TelecomRealnameService这个具体实现类，注册逻辑中的具体实现对象，是通过配置来注入的，达到了依赖倒置的目的</p>
<p>2、使用RealnameInfo这个DP来代替TelecomInfoDTO这个具体实现类</p>
<p>因此后续的业务逻辑只需要处理RealnameInfo，并不关注是哪个具体服务返回的结果并且DP具有逻辑内聚的好处，这样无论外部服务是参数变化还是替换实现，我们都将变动范围控制在了具体实现类和配置文件内部，这样就能保证核心业务逻辑的稳定</p>
<p>防腐层的概念</p>
<ul>
<li>防腐层，顾名思义就是防止外部系统腐烂影响到我们自己的系统，这里RealnameService就是我们的防腐层</li>
</ul>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">RealnameService</span> &#123;</span><br><span class="line">    RealnameInfo <span class="title function_">get</span><span class="params">(PhoneNumber phone)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TelecomRealnameService</span> <span class="keyword">implements</span> <span class="title class_">RealnameService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> RealnameInfo <span class="title function_">get</span><span class="params">(PhoneNumber phone)</span>&#123;</span><br><span class="line">        <span class="comment">// RPC调用，并将返回结果封装为RealnameInfo</span></span><br><span class="line">        <span class="comment">// RealnameInfo是DP</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SalesRepMapper salesRepDAO;</span><br><span class="line">    <span class="keyword">private</span> UserMapper userDAO;</span><br><span class="line">    <span class="keyword">private</span> RewardMapper rewardDAO;</span><br><span class="line">    <span class="keyword">private</span> RealnameService realnameService;</span><br><span class="line">    <span class="keyword">private</span> RiskControlService riskControlService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> UserDO <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="comment">// 一致性校验</span></span><br><span class="line">        <span class="type">RealnameInfo</span> <span class="variable">realnameInfo</span> <span class="operator">=</span> realnameService.get(phone);</span><br><span class="line">        realnameInfo.check(name);</span><br><span class="line">       </span><br><span class="line">        <span class="comment">// 计算标签信息</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">label</span> <span class="operator">=</span> getLabel(realnameInfo);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 计算销售组</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">salesRepId</span> <span class="operator">=</span> getSalesRepId(phone);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 构造对象</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">idCard</span> <span class="operator">=</span> realnameInfo.getIdCard();</span><br><span class="line">        <span class="type">UserDO</span> <span class="variable">userDO</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UserDO</span>(idCard, name, phone.getNumber(), label, salesRepId);</span><br><span class="line">        <span class="type">RewardDO</span> <span class="variable">rewardDO</span> <span class="operator">=</span> RewardDO(idCard, label);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 检查风控</span></span><br><span class="line">        <span class="keyword">if</span>(!riskControlService.check(idCard, label)) &#123;</span><br><span class="line">            userDO.setFresh(<span class="literal">true</span>);</span><br><span class="line">            rewardDO.setAvailable(<span class="literal">false</span>);</span><br><span class="line">        &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">            userDO.setFresh(<span class="literal">false</span>);</span><br><span class="line">            rewardDO.setAvailable(<span class="literal">true</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 存储信息</span></span><br><span class="line">        rewardDAO.insert(rewardDO);</span><br><span class="line">        <span class="keyword">return</span> userDAO.insert(userDO);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">getLabel</span><span class="params">(RealnameInfo info)</span> &#123;</span><br><span class="line">        <span class="comment">// 本地逻辑处理</span></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">getSalesRepId</span><span class="params">(PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="type">SalesRepDO</span> <span class="variable">repDO</span> <span class="operator">=</span> salesRepDAO.select(phone.getAreaCode(), phone.getOperatorCode());</span><br><span class="line">        <span class="keyword">if</span> (repDO != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> repDO.getRepId();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="抽象数据调用"><a href="#抽象数据调用" class="headerlink" title="抽象数据调用"></a>抽象数据调用</h3><p>1、DO作为数据表的直接映射，属于具体实现，不应该直接暴露给业务逻辑</p>
<p>2、DAO作为访问数据库的具体实现，不应该暴露给业务</p>
<p>这里引入领域驱动设计中的Entity和Repository，事实上，我们的业务逻辑应该只面向领域实体，而并不需要关心这个对象背后是否用到了数据库，如果用了也不需要关心用的什么数据库，是NoSql数据库还是ROBMS数据库都不关心，更不用关心数据表schema中定义了哪些字段，上层的业务逻辑并不需要关注下层的业务实现</p>
<p>User类的属性用于描述在这个系统内客户应该含有的信息，可以尽量多使用DP来将更多的自检和隐形属性内聚起来，在定义User类的时候，我们压根不关心下层数据库要怎么实现，甚至之后User对象的一部分信息存储在内存中，另一部分信息存储在数据库中也是无所谓了</p>
<p>在定义领域实体时，只关注如何去描述这个领域实体，Entity和DP的区别就是：</p>
<p>Entity：有状态，领域实体</p>
<p>DP：无状态，组成实体的基础类型</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// User Entity</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> &#123;</span><br><span class="line">    <span class="comment">// 用户id，DP</span></span><br><span class="line">    <span class="keyword">private</span> UserId userId;</span><br><span class="line">    <span class="comment">// 用户手机号，DP</span></span><br><span class="line">    <span class="keyword">private</span> PhoneNumber phone;</span><br><span class="line">    <span class="comment">// 用户标签，DP</span></span><br><span class="line">    <span class="keyword">private</span> Label label;</span><br><span class="line">    <span class="comment">// 绑定销售组ID，DP</span></span><br><span class="line">    <span class="keyword">private</span> SalesRepId salesRepId;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="type">Boolean</span> <span class="variable">fresh</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SalesRepRepository salesRepRepository;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造方法</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">User</span><span class="params">(RealnameInfo info, name, PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="comment">// 参数一致性校验，若校验失败，则check内抛出异常（DP的优点）</span></span><br><span class="line">        info.check(name);</span><br><span class="line">        initId(info);</span><br><span class="line">        labelledAs(info);</span><br><span class="line">        <span class="comment">// 查询</span></span><br><span class="line">        <span class="type">SalesRep</span> <span class="variable">salesRep</span> <span class="operator">=</span> salesRepRepository.find(phone);</span><br><span class="line">        <span class="built_in">this</span>.salesRepId = salesRep.getRepId();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 对this.userId赋值</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">initId</span><span class="params">(RealnameInfo info)</span> &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 对this.label赋值</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">labelledAs</span><span class="params">(RealnameInfo info)</span> &#123;</span><br><span class="line">        <span class="comment">// 本地处理逻辑</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">fresh</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.fresh = <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Repository：数据访问的抽象层，在抽象层，我们只定义动作，比如下面的UserRepository只定义了find和save两个动作，在UserRepository的实现类中就可以依赖数据库相关的各种实现</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserRepository</span> &#123;</span><br><span class="line">    User <span class="title function_">find</span><span class="params">(UserId id)</span>;</span><br><span class="line">    User <span class="title function_">find</span><span class="params">(PhoneNumber phone)</span>;</span><br><span class="line">    User <span class="title function_">save</span><span class="params">(User user)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserRepositoryImpl</span> <span class="keyword">implements</span> <span class="title class_">UserRepository</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> UserMapper userDAO;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> UserBuilder userBuilder;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">find</span><span class="params">(UserId id)</span> &#123;</span><br><span class="line">        <span class="type">UserDO</span> <span class="variable">userDO</span> <span class="operator">=</span> userDAO.selectById(id.value());</span><br><span class="line">        <span class="keyword">return</span> userBuilder.parseUser(userDO);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">find</span><span class="params">(PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="type">UserDO</span> <span class="variable">userDO</span> <span class="operator">=</span> userDAO.selectByPhone(phone.getNumber());</span><br><span class="line">        <span class="keyword">return</span> userBuilder.parseUser(userDO);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">save</span><span class="params">(User user)</span> &#123;</span><br><span class="line">        <span class="type">UserDO</span> <span class="variable">userDO</span> <span class="operator">=</span> userBuilder.fromUser(user);</span><br><span class="line">        <span class="keyword">if</span> (userDO.getId() == <span class="literal">null</span>) &#123;</span><br><span class="line">            userDAO.insert(userDO);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            userDAO.update(userDO);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> userBuilder.parseUser(userDO);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>现在的业务代码更加清晰了，最主要的是，核心业务逻辑不再依赖任何具体实现，无论什么外部依赖产生变动，我们只需要去修改相应的具体实现类，这样业务就比较稳定</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> UserRepository userRepository;</span><br><span class="line">    <span class="keyword">private</span> RewardRepository rewardRepository;</span><br><span class="line">    <span class="keyword">private</span> RealnameService realnameService;</span><br><span class="line">    <span class="keyword">private</span> RiskControlService riskControlService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> UserDO <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="comment">// 查询实名信息</span></span><br><span class="line">        <span class="type">RealnameInfo</span> <span class="variable">realnameInfo</span> <span class="operator">=</span> realnameService.get(phone);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 构造对象</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>(realnameInfo, phone);</span><br><span class="line">        <span class="type">Reward</span> <span class="variable">reward</span> <span class="operator">=</span> Reward(user);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 检查风控</span></span><br><span class="line">        <span class="keyword">if</span>(!riskControlService.check(user)) &#123;</span><br><span class="line">            user.fresh();</span><br><span class="line">            reward.inavailable();</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 存储信息</span></span><br><span class="line">        rewardRepository.save(reward);</span><br><span class="line">        <span class="keyword">return</span> UserRepository.save(user);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="综合改造后"><a href="#综合改造后" class="headerlink" title="综合改造后"></a>综合改造后</h3><p>由内部逻辑的变化所导致的内部系统的改造程度，我们可以狭义地理解为系统的可拓展性</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegistrationServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">RegistrationService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> UserRepository userRepository;</span><br><span class="line">    <span class="keyword">private</span> RealnameService realnameService;</span><br><span class="line">    <span class="keyword">private</span> CheckUserService checkUserService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> UserDO <span class="title function_">register</span><span class="params">(String name, PhoneNumber phone)</span> &#123;</span><br><span class="line">        <span class="comment">// 查询信息</span></span><br><span class="line">        <span class="type">RealnameInfo</span> <span class="variable">realnameInfo</span> <span class="operator">=</span> realnameService.get(phone);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 构造对象</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>(realnameInfo, phone);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 检查并更新对象</span></span><br><span class="line">        checkUserService.check(user);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 存储信息</span></span><br><span class="line">        <span class="keyword">return</span> userRepository.save(user);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这里的CheckUserService中进行检查时，可能会改变User对象和Reward的对象的状态，设计到了多个Entity状态改变的服务被称为Domain Service，Domain Service主要用于封装多个Entity或者跨业务域的逻辑</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">CheckUserService</span>(User user) &#123;</span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">check</span><span class="params">(user)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CheckAndUpdateUserServiceImpl</span>(User user) &#123;</span><br><span class="line">    <span class="keyword">private</span> RiskControlService riskControlService;</span><br><span class="line">    <span class="keyword">private</span> RewardRepository rewardRepository;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">check</span><span class="params">(User user)</span> &#123;</span><br><span class="line">        rewardCheck(user);</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">        <span class="comment">// 可能存在的其他逻辑</span></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">rewardCheck</span><span class="params">(User user)</span> &#123;</span><br><span class="line">        <span class="type">Reward</span> <span class="variable">reward</span> <span class="operator">=</span> Reward(user);</span><br><span class="line">        <span class="comment">// 检查风控</span></span><br><span class="line">        <span class="keyword">if</span>(!riskControlService.check(user)) &#123;</span><br><span class="line">            user.fresh();</span><br><span class="line">            reward.inavailable();</span><br><span class="line">        &#125;</span><br><span class="line">        rewardRepository.save(reward);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>简单概念归纳</p>
<ul>
<li>DP：抽象并封装自检和一些隐形属性的计算逻辑，且这些属性是无状态的</li>
<li>Entity：抽象并封装单对象有状态的逻辑</li>
<li>Domain Service：抽象并封装多对象的有状态逻辑</li>
<li>Repository：抽象并封装外部数据访问逻辑</li>
</ul>
<p>在领域驱动设计下写代码是这么一个过程：</p>
<p>1、首先对需要处理的业务问题进行总览</p>
<p>2、然后领域对象（Entity）进行划分，明确每个领域对象的包含的信息和职责边界。并进行跨对象，多对象的逻辑组织（Domain Service）</p>
<p>3、接着在上层应用中根据业务描述去编排Entity和Domain Service</p>
<p>4、最后再做一下下水道工作，去对下层的数据访问，RPC调用之类的做一些具体实现</p>
<h2 id="统一语言和模型价值"><a href="#统一语言和模型价值" class="headerlink" title="统一语言和模型价值"></a>统一语言和模型价值</h2><p>统一语言UL（Ubiquitous Language）</p>
<p>VO存在的歧义</p>
<table>
<thead>
<tr>
<th></th>
<th>DDD语境</th>
<th>传统语境</th>
</tr>
</thead>
<tbody><tr>
<td>含义</td>
<td>ValueObject</td>
<td>ViewObject&#x2F;ValueObject</td>
</tr>
<tr>
<td>作用</td>
<td>领域基础类型</td>
<td>用于展示的数据</td>
</tr>
</tbody></table>
<p>借贷存在的歧义</p>
<table>
<thead>
<tr>
<th></th>
<th>财务人眼中</th>
<th>技术人眼中</th>
</tr>
</thead>
<tbody><tr>
<td>含义</td>
<td>资产&#x3D;负债+所有者权益</td>
<td>借钱？贷款？</td>
</tr>
</tbody></table>
<p>为了形成可靠的UL，当不同角色的同事在沟通时一旦产生新的术语，需要双方不断确认，形成一致的理解，并且在后续的沟通中，反复使用新产生的术语，一旦有人觉得术语产生歧义，那就可以及时修正大家对它的理解形成共识。</p>
<p>在不断强化UL的过程中，其实也是在对业务领域知识进行消化，为后续建设丰富可靠的模型打基础，所以领域驱动设计提倡的 是业务领域专家和技术人员一起去进行螺旋式的建模，通过形成UL消化业务知识，然后再想模型中提炼知识，依次往复</p>
<p><img src="https://pic1.zhimg.com/80/v2-eea685d89d3af4a78585ee57b168a060_720w.webp" class="lazyload placeholder" data-srcset="https://pic1.zhimg.com/80/v2-eea685d89d3af4a78585ee57b168a060_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>“好的模型价值千金”，什么是模型？模型的价值究竟在哪？</p>
<p>举个例子</p>
<p>星盘是由古希腊的天文学家发明的，在中世纪伊斯兰科学家又对它进行了改进，星盘上可旋转的铜环代表着各恒星在天体上的位置，刻有当地地平坐标系的盘面是可替换的，它代表了不同纬度的星空景象，在星盘的盘面旋转铜环，可以计算出全年任何的天体位置，相似的，如果知道某个恒星在天体上的位置，也可以因此计算出时间。</p>
<p>在这里，星盘使用机械的方式，建立了恒星星空和时间的面向对象模型，在这个案例下，你也可以使用软件来实现这个星空模型，但首先需要向了解天文学家的专家，学习天文领域的知识，然后建模，如果你的模型是良好的，那么就不能因为更改了坐标系而失效，也不能因为新加入了一颗恒星就失效。</p>
<p>领域驱动设计就是一套方法论，去帮助我们建设良好的模型</p>
<p><img src="https://pic3.zhimg.com/80/v2-34cecb203a6facaf07ae2d28aadfb79a_720w.webp" class="lazyload placeholder" data-srcset="https://pic3.zhimg.com/80/v2-34cecb203a6facaf07ae2d28aadfb79a_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>是否需要领域模型</p>
<p>在平时的工作中，想要建设良好的模型其实是困难重重的，从简单到复杂大致存在以下几种情况：</p>
<table>
<thead>
<tr>
<th></th>
<th>业务简单</th>
<th>业务逐渐复杂</th>
<th>业务复杂</th>
</tr>
</thead>
<tbody><tr>
<td>可能的状况</td>
<td>无需过多投入，若业务深耕，则需要引入良好的模型</td>
<td>考验产品经历是否能兼任领域专家</td>
<td>分工明确，关注领域信息</td>
</tr>
<tr>
<td>模型价值</td>
<td>低</td>
<td>中</td>
<td>高</td>
</tr>
</tbody></table>
<p>为了建设有价值的模型，我们需要在形成UL的基础上消化知识，并向模型中提炼知识</p>
<p>DP和Entity的区别</p>
<table>
<thead>
<tr>
<th></th>
<th>DP（VO）</th>
<th>Entity</th>
</tr>
</thead>
<tbody><tr>
<td>是否充血</td>
<td>是</td>
<td>是</td>
</tr>
<tr>
<td>有无状态</td>
<td>无</td>
<td>有</td>
</tr>
</tbody></table>
<p>实体可以持久化，VO只是用于数据传输</p>
<p>一个简单的充血案例</p>
<p>“充血模型”的英文原文是”rich domain model”，所谓充血，就是赋予对象除了属性和属性的读写方法外包含业务逻辑的领域行为，这样的好处是模型本身高度内聚，表达力很强，而不是每设计一个业务操作，就交由其它的对象处理，这样就能够有效避免代码写成高度耦合的，且难以维护的事务脚本，这也涉及到了充血程度的问题，哪些逻辑应该被当前对象包含，哪些逻辑应该被拆分到其它对象中，这里没有金科玉律，只能结合实际的问题。</p>
<p>理想状态下，充血模型应该是自治的、丰富的、有活力的。DP和Entity的区别是有无状态</p>
<p>什么叫状态？</p>
<p>可以从两个角度理解：</p>
<p>1、该对象是否存在生命周期</p>
<p>2、程序是否需要追踪该对象的变化事件</p>
<p>这会有点抽象，举个例子：</p>
<p>总所周知，体育馆内有很多座位，假如观众通过预定程序买了票，每张票对应着一个座位号，这种场景下，座位就是有状态的，它是Entity，程序需要关注该对象的变化事件，比如通过唯一ID来追踪该对象，关注它的预定状态、价格、位置等属性</p>
<p>另一种场景下，假如观众通过预定程序买了票，进场只要有空座位就可以随便坐，这时候，座位就是无状态的，它是DP，程序不需要对每个座位对象进行追踪，只需要关注总数即可，这就是DP和Entity的异同之处</p>
<p>一句话总结就是：</p>
<p>为了建设有价值的模型，在确认UL之后消化领域知识，向无状态的DP和有状态的Entity中提炼知识</p>
<p><img src="https://pic4.zhimg.com/80/v2-cf63eaa0cee63fb653511df9884cdb73_720w.webp" class="lazyload placeholder" data-srcset="https://pic4.zhimg.com/80/v2-cf63eaa0cee63fb653511df9884cdb73_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<h2 id="聚合根-amp-限界上下文"><a href="#聚合根-amp-限界上下文" class="headerlink" title="聚合根&amp;限界上下文"></a>聚合根&amp;限界上下文</h2><p>聚合：Aggregate</p>
<p>聚合：AggregateRoot</p>
<p>限界上下文：BoundedContext</p>
<h3 id="聚合的概念"><a href="#聚合的概念" class="headerlink" title="聚合的概念"></a>聚合的概念</h3><p>思考这么一个问题：</p>
<p>假设微信账号的主键就是手机号，现在用户A想要注销自己的微信账号，那么系统是否需要同时通过A的手机号删除关联的微信钱包信息？</p>
<p>如果不需要，那假如该手机号被用户弃用，之后被运营商回收后发给用户B，用户B再用这个手机号注册微信时会发现自己的账号中居然已经绑定了其它人的微信钱包</p>
<p>如果需要删除那是否需要删除绑定在钱包中的银行卡信息？</p>
<p>如果这张银行卡不仅绑定在了A的账号里，还作为亲属卡绑定在了A的儿子小王的账号里，那么删除这张银行卡的绑定信息是否会影响到小王的钱包使用？</p>
<p><img src="https://pic3.zhimg.com/80/v2-4d2460ab9f8704c031040c3926193dd6_720w.webp" class="lazyload placeholder" data-srcset="https://pic3.zhimg.com/80/v2-4d2460ab9f8704c031040c3926193dd6_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>从上面这个简单的例子，可以想象出：</p>
<p>在一个复杂的系统中，对一个对象进行修改，可能会涉及到大量其它关联对象的状态，如何使这些对象的状态始终保持一致是一个复杂的话题，从这个例子来看，对象之间的关系，有点像是数据结构中的图</p>
<p>我们用Aggregate(聚合)来描述这种存在引用关系的对象集合，聚合是对存在引用关系的一组对象的封装，它的目的就是屏蔽掉内部对象之间复杂的关联关系，只对外暴露统一接口</p>
<p><img src="https://pic3.zhimg.com/80/v2-925033134012fd50828e44ed3ff415e2_720w.webp" class="lazyload placeholder" data-srcset="https://pic3.zhimg.com/80/v2-925033134012fd50828e44ed3ff415e2_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>关于聚合，我们需要关注它的两个属性：根对象和边界</p>
<p>根对象是整个聚合中唯一能够被外部引用的对象，也就是说，聚合所暴露的接口，只允许操作根对象，根对象是一个Entity，因为每个聚合需要ID和状态来区分其它聚合，所以这里也就是通过根对象的ID来作为整个聚合的ID</p>
<p>边界，简单来说，聚合的边界就是你判断哪些对象可以被放入当前聚合的条件</p>
<p>利用这些概念，我们从DDD的角度来审视一下上面的那个例子</p>
<p><img src="https://pic4.zhimg.com/80/v2-8f48fbd57bc7126a6c00fec0f35a18cf_720w.webp" class="lazyload placeholder" data-srcset="https://pic4.zhimg.com/80/v2-8f48fbd57bc7126a6c00fec0f35a18cf_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>因为注销账号涉及到了账号信息、钱包信息、钱包信息又涉及到绑定银行卡信息，所以我们可以将着三个对象封装为一个聚合，既然定义了一个聚合，我们就需要关注两个属性：根和边界</p>
<p>该聚合的边界就是进行账号注销时，当前对象是否被引用？比如聊天消息对象，当用户A的账号被注销，它与其它用户之间已经产生的聊天信息并不需要被清空，所以聊天消息对象并不能通过边界进入到这个聚合中</p>
<p>这个聚合的根对象又是什么呢？当前聚合是为了解决包含注销账号的一系列操作，那么毫无疑问，根对象就是账号对象</p>
<p>聚合外部可以持有根对象的引用来操作聚合，而聚合内部通过执行一系列逻辑来保持各个对象状态的一致</p>
<h3 id="DP-x2F-VO、Entity的复习"><a href="#DP-x2F-VO、Entity的复习" class="headerlink" title="DP&#x2F;VO、Entity的复习"></a>DP&#x2F;VO、Entity的复习</h3><p>每个Entity都是存在标识的，标识一般是不可变类型，并不是所有的DP都是用来作为标识的</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 这里定义一个Identifier接口，用于所有作为DP的公共父接口</span></span><br><span class="line"><span class="comment">// 也就是说，这里不推荐使用String类型作为Entity的标识，建议使用DP来作为Entity的标识</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Identifier</span> <span class="keyword">extends</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义了Entity接口，它的泛型是Identify的子类，该泛型指定了Entity的标识类型，也就是刚才说的</span></span><br><span class="line"><span class="comment">// 实现了Identifier接口的DP</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Entity</span>&lt;T <span class="keyword">extends</span> <span class="title class_">Identifier</span>&gt; &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 手机号可以作为Identify的标识，所以手机号PhoneNumber实现了Identifier</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PhoneNumber</span> <span class="keyword">implements</span> <span class="title class_">Identifier</span> &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这里定义了一个DP，Red，来表示红色，它就不适合作为Entity的标识</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Red</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String desc；</span><br><span class="line">        <span class="keyword">private</span> String rgb;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这里的AggregateRoot继承了Entity</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">AggregateRoot</span>&lt;T <span class="keyword">extends</span> <span class="title class_">Identifier</span> &gt; <span class="keyword">extends</span> <span class="title class_">Entity</span>&lt;T&gt;&#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义账号类WechatAccount，它是一个Entity</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WechatAccount</span> <span class="keyword">implements</span> <span class="title class_">AggregateRoot</span>&lt;PhoneNumber&gt; &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>聚合根是Entity，所以上面的AggregateRoot继承了Entity，和Entity一样，聚合根也需要一个标识，所以这里指定泛型为Identifier的子类</p>
<p>结合上面的例子，这里先定义账号类，它是一个Entity，WechatAccount的泛型为PhoneNumber，意味着手机号是每个微信账号的全局标识，在账号操作这个业务聚合中，它能够充当被外部引用的根</p>
<h3 id="简单的转账需求"><a href="#简单的转账需求" class="headerlink" title="简单的转账需求"></a>简单的转账需求</h3><p>业务需求：</p>
<p>微信用户A转账给微信用户B</p>
<p>这个场景涉及到了三种类型的对象：余额、钱包、微信账号</p>
<p>所以下面建了三个类，三个类的类型对象都是Entity，其中WechatAccount是聚合根，三个类中都加上了私有的无参构造器，这么做的目的是为了拒绝通过new来构建对象，而是通过工厂来初始化符合标准的对象</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Balance</span> <span class="keyword">implements</span> <span class="title class_">Entity</span>&lt;BalanceNumber&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> BalanceNumber id;</span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="keyword">private</span> <span class="title function_">BalanceNumber</span><span class="params">()</span> &#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Wallet</span> <span class="keyword">implements</span> <span class="title class_">Entity</span>&lt;WalletNumber&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> WalletNumber id;</span><br><span class="line">    <span class="keyword">private</span> Balance balance; <span class="comment">// 余额</span></span><br><span class="line">    <span class="keyword">private</span> Statement statement； <span class="comment">// 账单明细</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="keyword">private</span> <span class="title function_">Wallet</span><span class="params">()</span> &#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WechatAccount</span> <span class="keyword">implements</span> <span class="title class_">AggregateRoot</span>&lt;PhoneNumber&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> PhoneNumber id;</span><br><span class="line">    <span class="keyword">private</span> Nickname nickname; </span><br><span class="line">    <span class="keyword">private</span> Wallet wallet;</span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="keyword">private</span> <span class="title function_">WechatAccount</span><span class="params">()</span> &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>用户A转账给用户B，这个操作涉及到了多个Entity的状态变化，所以下面简单设计了一个Domain Service，在TransferService中注入了数据操作对象AccountRepository，Repository仓储层的具体实现中包含了数据操作，可能是更新多张数据表，也可能是刷新缓存等等</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// domain service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TransferServiceImpl</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> AccountRepository repository;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">transfer</span><span class="params">(WechatAccount payer, WechatAccount payee, Asset asset)</span> &#123;</span><br><span class="line">        payer.withdraw(asset);</span><br><span class="line">        payee.deposit(asset);</span><br><span class="line">        repository.save(payer);</span><br><span class="line">        repository.save(payee);</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>transfer方法中的简化逻辑就是付款方扣钱，收款方收钱，然后保存数据</p>
<p>来关注一下付款方，当付款方需要转钱出去，在用户转账的这个业务领域下，一定包含多个对象的状态变化，这些变化的对象就是一个聚合，WechatAccount作为聚合根，它的引用能够被外部获取并保持，而Wallet对象处于这个聚合的内部，它的引用只允许被内部持有，不允许被外部持有，所以TransferService只会持有WechatAccount类型的对象</p>
<p><img src="https://pic3.zhimg.com/80/v2-9b3182f5671ceaa4b38a2f15c71b8c42_720w.webp" class="lazyload placeholder" data-srcset="https://pic3.zhimg.com/80/v2-9b3182f5671ceaa4b38a2f15c71b8c42_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<h3 id="聚合根初体验"><a href="#聚合根初体验" class="headerlink" title="聚合根初体验"></a>聚合根初体验</h3><p>聚合根处理逻辑，通过操作聚合根，简洁又安全的实现了聚合中所有关联对象，在运行时的状态一致，此外，在对象进行持久化时，如果我也只需要操作聚合根，那就太方便了，这里提现出了DDD的一种思想</p>
<p>在业务层，开发者只要专注于领域的逻辑，不需要处理如何将对象和数据表进行映射，也就是拒绝面向数据表进行增删改查</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WechatAccount</span> <span class="keyword">implements</span> <span class="title class_">AggregateRoot</span>&lt;PhoneNumber&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> PhoneNumber id;</span><br><span class="line">    <span class="keyword">private</span> Nickname nickname; </span><br><span class="line">    <span class="keyword">private</span> Wallet wallet;</span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="keyword">private</span> <span class="title function_">WechatAccount</span><span class="params">()</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">withdraw</span><span class="params">(Asset asset)</span> &#123;</span><br><span class="line">        wallet.pay(asset);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deposit</span><span class="params">(Asset asset)</span> &#123;</span><br><span class="line">        wallet. receive(asset);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Wallet</span> <span class="keyword">implements</span> <span class="title class_">Entity</span>&lt;WalletNumber&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> WalletNumber id;</span><br><span class="line">    <span class="keyword">private</span> Balance balance; <span class="comment">// 余额</span></span><br><span class="line">    <span class="keyword">private</span> Statement statement； <span class="comment">// 账单明细</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="keyword">private</span> <span class="title function_">Wallet</span><span class="params">()</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> pay <span class="keyword">throws</span> <span class="title function_">BalanceException</span><span class="params">(Asset asset)</span> &#123;</span><br><span class="line">        balance.decrease(asset.getBalance);</span><br><span class="line">        statement.add(asset);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> receive <span class="keyword">throws</span> <span class="title function_">BalanceException</span><span class="params">(Asset asset)</span> &#123;</span><br><span class="line">        balance.increase(asset.getBalance);</span><br><span class="line">        statement.add(asset);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure>

<h3 id="数据访问"><a href="#数据访问" class="headerlink" title="数据访问"></a>数据访问</h3><p>回过头看这里的account repository，repository作为数据操作组件屏蔽了上层领域逻辑和下层的数据库实现，通过repository，我们就能够只操作聚合根来实现关联对象在持久化时的状态一致</p>
<p>所以我们应该对repository有如下的类定义：</p>
<p>repository的泛型有两个：</p>
<ul>
<li>AggregateRoot的子类</li>
<li>AggregateRoot的标识类型</li>
</ul>
<p>代表repository能够接受和处理的聚合根以及聚合根的标识</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Repository</span>&lt;T <span class="keyword">extends</span> <span class="title class_">AggregateRoot</span>&lt;ID&gt;, ID <span class="keyword">extends</span> <span class="title class_">Identifier</span>&gt; &#123;</span><br><span class="line">    save(T t);</span><br><span class="line">    WechatAccount <span class="title function_">find</span><span class="params">(ID id)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>AccountRepository将是Repository的子接口，它处理的聚合根类型是WechatAccount，WechatAccount的标识是PhoneNumber</p>
<p>通过上面的分析我们知道当用户间进行转账时，涉及账号对象、钱包对象、余额对象、明细对象的变化，AccountRepository中的find方法就是通过手机号对象去查找整个聚合，save方法则是持久化当前聚合中所有对象的状态</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">AccountRepository</span> <span class="keyword">extends</span> <span class="title class_">Repository</span>&lt;WechatAccount, PhoneNumber&gt; &#123;</span><br><span class="line">    save(WechatAccount wechatAccount);</span><br><span class="line">    WechatAccount <span class="title function_">find</span><span class="params">(PhoneNumber phoneNumber)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Entity是对业务领域的建模，而不是对数据表的映射建模，PO才是对数据表的映射，所以Entity和PO之间可能存在一对一、一对多、多对一的关系，比如一个余额对象，可能存在可能余额和冻结余额这两个属性，这两个属性很可能是存储在可用余额表和冻结余额表两张数据表中的</p>
<p>在这里Entity和PO就是一对多的关系，在当前业务场景中，我们对问题进行简化，假设账号信息、钱包信息、余额信息、明细信息都分别只是一张表，在这里Entity和PO都是一对一的关系，理所应当的Entity和PO之间需要专门的**转换器(convert)**来进行类型转换</p>
<p><img src="https://pic1.zhimg.com/80/v2-9221b3f6b7a8db11110194749301bd30_720w.webp" class="lazyload placeholder" data-srcset="https://pic1.zhimg.com/80/v2-9221b3f6b7a8db11110194749301bd30_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>在WeAccountRepositoryImpl这个具体实现类中，通过持有和传递聚合根，完成了对所有关联对象的持久化，但仔细观察save方法内的逻辑，还是有可优化的空间</p>
<p>因为WechatAccount是充血的，比如在类定义中，除了这里关注的wallet还有个nickname属性，假如一个用户想要修改他的昵称，在nickname这个DP被修改后，也会调用AccountRepository的save方法来保持数据，而save的逻辑中，会把当前的WechatAccount对象中的所有数据都保存一遍，这里存在很多不必要的数据库调用，非常具有优化价值</p>
<p>总结一下：根本的问题所在就是我们现在不知道WechatAccount这个对象中的哪些属性被修改了需要保存，哪些没有被修改可以不用保存</p>
<p>怎么修改？这被称作change tracking问题</p>
<p>思路：</p>
<p>1、记录聚合中每个对象的版本号，如果被修改，那么版本号自增</p>
<p>2、在进行save的时候检查版本号，比如可以对这些对象进行切面代理，当对象被修改，代理中就标记为已修改，在save的时候检查这个标记</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WeAccountRepositoryImpl</span> <span class="keyword">implements</span> <span class="title class_">AccountRepository</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> WechatAccountDAO wechatAccountDAO;</span><br><span class="line">    <span class="keyword">private</span> WalletDAO  walletDAO;</span><br><span class="line">    <span class="keyword">private</span> BalanceDAO balanceDAO;</span><br><span class="line">    <span class="keyword">private</span> StatementDAO statementDAO;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">save</span><span class="params">(WechatAccount wechatAccount)</span> &#123;</span><br><span class="line">        <span class="type">WechatAccountPO</span> <span class="variable">wechatAccountPO</span> <span class="operator">=</span> WechatAccountConverter.convert(wechatAccount);</span><br><span class="line">        wechatAccountDAO.save(wechatAccountPO);</span><br><span class="line"></span><br><span class="line">        <span class="type">Wallet</span> <span class="variable">wallet</span> <span class="operator">=</span> WalletFactory.obtain(wechatAccount);</span><br><span class="line">        <span class="type">WalletPO</span> <span class="variable">walletPO</span> <span class="operator">=</span> walletConverter.convert(wallet);</span><br><span class="line">        walletDAO.save(walletPO);</span><br><span class="line"></span><br><span class="line">        <span class="type">Balance</span> <span class="variable">balance</span> <span class="operator">=</span> BalanceFactory.obtain(wallet);</span><br><span class="line">        <span class="type">BalancePO</span> <span class="variable">balancePO</span> <span class="operator">=</span> balanceConverter.convert(balance);</span><br><span class="line">        balanceDAO.save(balancePO);</span><br><span class="line"></span><br><span class="line">        <span class="type">Statement</span> <span class="variable">statement</span> <span class="operator">=</span> StatementFactory.obtain(wallet);</span><br><span class="line">        <span class="type">StatementPO</span> <span class="variable">statementPO</span> <span class="operator">=</span> statementConverter.convert(statement);</span><br><span class="line">        statementDAO.save(statementPO);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//... 其他WechatAccount的属性</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    WechatAccount <span class="title function_">find</span><span class="params">(PhoneNumber phoneNumber)</span> &#123;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>关于聚合，总结一下：</p>
<p><img src="https://pic3.zhimg.com/80/v2-d41225fee828ccd51fcde8d9f14e6252_720w.webp" class="lazyload placeholder" data-srcset="https://pic3.zhimg.com/80/v2-d41225fee828ccd51fcde8d9f14e6252_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>在复杂的软件中，一个业务动作会涉及大量存在关联的对象，聚合的价值就是通过封装，保持所有关联对象的状态一致</p>
<p>此外聚合存在两个必须关注的属性：根对象和边界，明确了这两个属性，整个聚合的面貌就清晰了</p>
<p>在运行时，领域方法通过持有聚合根维护了对象的状态一致，在持久化时时，repository通过持有聚合根来处理数据一致</p>
<p>最后额外提到的是，因为聚合中存在大量对象，持久化时我们希望每次数据更新保持最小化原则</p>
<h3 id="限界上下文的概念"><a href="#限界上下文的概念" class="headerlink" title="限界上下文的概念"></a>限界上下文的概念</h3><p>限界上下文：BoundedContext</p>
<p>这个概念一直被解释的比较抽象，它的本质是为了解决复杂系统的领域分治问题，这并不仅仅是DDD这种架构方式需要面临和处理的问题，其它任何架构方式，都需要处理类似问题</p>
<p>领域分治最清晰的方式就是通过拆分成微服务，但拆分成微服务本身也存在边界问题，而且将会增加通信的复杂度，其次有些领域能力本身就适合作为一个集合，由一个系统来提供，不适合按照每种能力拆分成微服务</p>
<p>在一个系统内，如果进行领域分治也是一个有价值的话题，在DDD中，就是通过BC（BoundedContext）这种思想来解决</p>
<p><img src="https://pic1.zhimg.com/80/v2-9c21f618d26bdfce8c639b03cc054bc0_720w.webp" class="lazyload placeholder" data-srcset="https://pic1.zhimg.com/80/v2-9c21f618d26bdfce8c639b03cc054bc0_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>以一个支付系统为例：</p>
<p>假设这个支付系统提供了支付、退款、冻结、解冻，4种领域能力</p>
<p>BC可以从两个方面来发挥作用：</p>
<p>1、领域隔离；可以料想，在这四个领域中，都需要承载支付单这个语义的Entity，在数据表中它可能也只对应一条数据，但是在支付和退款这两个领域中，因为看待支付单的角度不同，业务逻辑的侧重点也不一样，所以建模方式也会存在差异，这些差异是否需要区分，从DDD的方式去看，它们应该使用不同的实现通过BC来进行隔离，这样假如系统变得越来越复杂，两者支付单对象的差异也越来越大时，在一个BC中的改动就会很安全</p>
<p>2、模型隔离；当一个系统变得庞大时或者经历了长时间的迭代，在几代开发人员的修改维护重构下，它的内部可能催生出多套模型，多代架构，它们往往目前处于共同运行的状态，甚至还会共用一部分逻辑和代码，但目标是从老模型彻底迁移到新模型，假设这个系统的冻结领域按照时间的发展，目前存在三套模型，这三套模型应该使用BC来尽可能的隔离起来，模型之间交叉的地方，通过实现转换器来适配，这样在开发人员无法对多套模型的运作都了如指掌的情况下，在一套模型中修改将会是相对安全的</p>
<p><img src="https://pic2.zhimg.com/80/v2-d2c639dab399caab2d18b5f5ea5b3619_720w.webp" class="lazyload placeholder" data-srcset="https://pic2.zhimg.com/80/v2-d2c639dab399caab2d18b5f5ea5b3619_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>BC是一种思想，包含了如下的多种设计模型</p>
<p><img src="https://pic1.zhimg.com/80/v2-e0c08556ae2a6208e53c83d9d99ad998_720w.webp" class="lazyload placeholder" data-srcset="https://pic1.zhimg.com/80/v2-e0c08556ae2a6208e53c83d9d99ad998_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<h2 id="Exercise"><a href="#Exercise" class="headerlink" title="Exercise"></a>Exercise</h2><p>美团博客原文：<a target="_blank" rel="noopener" href="https://tech.meituan.com/2017/12/22/ddd-in-practice.html">https://tech.meituan.com/2017/12/22/ddd-in-practice.html</a></p>
<p>原文需求：</p>
<p>1、抽奖活动有活动限制，例如用户的抽奖次数限制，抽奖的开始和结束的时间等；<br>2、一个抽奖活动包含多个奖品，可以针对一个或多个用户群体；<br>3、奖品有自身的奖品配置，例如库存量，被抽中的概率等，最多被一个用户抽中的次数等等；<br>4、用户群体有多种区别方式，如按照用户所在城市区分，按照新老客区分等；<br>5、活动具有风控配置，能够限制用户参与抽奖的频率。</p>
<p>将需求初步抽象：</p>
<p>1、参与抽象需要一些限制条件【风控支持】</p>
<p>2、一次抽奖将多个奖品发放给多个用户【抽奖逻辑】</p>
<p>3、奖品的属性：库存量、抽中概率、同一用户重复抽中次数上限等【库存支持】</p>
<p>4、用户的属性：所在城市、新老标签【用户信息】</p>
<p>5、活动需要风控限制刷单【风控支持】</p>
<p>从业务上可以把抽奖领域划分为：抽奖子域、库存子域、风控子域</p>
<p>库存子域只是涉及库存的相关逻辑，主要用于和库存领域的系统通信，库存相关的核心逻辑还是在库存领域中，风控子域同理， 每个自于之间可能存在通信的需求，这将依赖每个子域的限界上下文来实现</p>
<p>限界上下文</p>
<p>这个领域很像细胞，每个子域的边界就像细胞壁，只有满足一定条件的限界上下文才允许通信，限界上下文存在的关系被称为context map</p>
<p>限界上下文之间可能存在的关系</p>
<p>1、合作关系（Partnership）：两个上下文紧密合作的关系，一荣俱荣，一损俱损</p>
<p>2、共享内核（Shared Kernel）：两个上下文依赖部分共享的模型</p>
<p>3、客户方-供应方开发（Customer-Supplier Development）：上下文之间有组织的上下游依赖</p>
<p>4、遵奉者（Conformist）：下游上下文只能盲目依赖上游上下文</p>
<p>5、防腐层（Anticorruption Layer）：一个上下文通过一些适配和转换与另一个上下文交互</p>
<p>6、开放主机服务（Open Host Service）：定义一种协议来让其它上下文来对本上下文进行访问</p>
<p>7、发布语言（Published Language）：通常与OHS一起使用，用于定义开放主机的协议</p>
<p>8、大泥球（Big Ball of Mud）：混杂在一起的上下文，边界不清晰</p>
<p>9、另谋他路（SeparateWay）：两个完全没有联系的上下文</p>
<p><img src="https://pic4.zhimg.com/80/v2-4a612c013a93af5005dc265b304b5497_720w.webp" class="lazyload placeholder" data-srcset="https://pic4.zhimg.com/80/v2-4a612c013a93af5005dc265b304b5497_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>抽奖子域和库存子域之间属于什么关系？</p>
<p>抽象活动依赖于商品库存，但是商品库存不会依赖抽奖活动，所以属于客户方-供应方的关系</p>
<p>同理抽奖依赖风控，但是库存和风控之间没有任何依赖关系</p>
<p>模块结构组织</p>
<p><img src="https://pic2.zhimg.com/80/v2-0b525da9bfbeb23196ae3e6284c8cf11_720w.webp" class="lazyload placeholder" data-srcset="https://pic2.zhimg.com/80/v2-0b525da9bfbeb23196ae3e6284c8cf11_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>同一域下模型隔离方式：</p>
<p>1、同module下，使用不同包路径</p>
<p>2、不同module下，同bean context</p>
<p>3、不同module，不同bean context</p>
<p>建模</p>
<ul>
<li>值对象（DP&#x2F;VO）</li>
<li>实体（Entity）</li>
<li>聚合（Aggregate）</li>
<li>聚合根（AggregateRoot）</li>
</ul>
<p><img src="https://pic2.zhimg.com/80/v2-c58ddedad87f6a169a3cb2d72e15f16d_720w.webp" class="lazyload placeholder" data-srcset="https://pic2.zhimg.com/80/v2-c58ddedad87f6a169a3cb2d72e15f16d_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>
<p>寒食君觉得这个建模方式存在两类问题：</p>
<p>第一类，值对象（VO）和实体（Entity）搞混了，DrawLottery它的ID是lotteryId类型，lotteryId本身是不可变的，应该是一个DP或者说VO，而且文章后面的代码部分使用了int来声明lotteryId，这是前后矛盾的，所以他认为lotteryId应该是VO而不是Entity，此外Award对象代表奖品，奖品是有ID且存在状态的，比如库存数量就是它的状态，Award应该是Entity，同理UserGroup、UserPool都应该是Entity</p>
<p>第二类，聚合，聚合根的使用姿势不太对，SendResult这个类型作为抽奖结果是Entity没错，但它作为抽奖的结果，状态需要和抽奖保持同步一致，也就是说drawlottery和sendresult，应该存在于一个聚合内，这个聚合的聚合根为drawlottery，此外，抽奖和发奖，这两个操作是异步的，所以发奖记录UserLotteryLog作为一个Entity实体不存在这个聚合中，这是合理的</p>
<h3 id="原文写法"><a href="#原文写法" class="headerlink" title="原文写法"></a>原文写法</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.company.team.bussiness.lottery.domain.aggregate;</span><br><span class="line"><span class="keyword">import</span> ...;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DrawLottery</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> lotteryId; <span class="comment">//抽奖id</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;AwardPool&gt; awardPools; <span class="comment">//奖池列表</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//getter &amp; setter</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setLotteryId</span><span class="params">(<span class="type">int</span> lotteryId)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span>(id&lt;=<span class="number">0</span>)&#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;非法的抽奖id&quot;</span>); </span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">this</span>.lotteryId = lotteryId;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//根据抽奖入参context选择奖池</span></span><br><span class="line">    <span class="keyword">public</span> AwardPool <span class="title function_">chooseAwardPool</span><span class="params">(DrawLotteryContext context)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span>(context.getMtCityInfo()!=<span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> chooseAwardPoolByCityInfo(awardPools, context.getMtCityInfo());</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> chooseAwardPoolByScore(awardPools, context.getGameScore());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//根据抽奖所在城市选择奖池</span></span><br><span class="line">    <span class="keyword">private</span> AwardPool <span class="title function_">chooseAwardPoolByCityInfo</span><span class="params">(List&lt;AwardPool&gt; awardPools, MtCifyInfo cityInfo)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span>(AwardPool awardPool: awardPools) &#123;</span><br><span class="line">            <span class="keyword">if</span>(awardPool.matchedCity(cityInfo.getCityId())) &#123;</span><br><span class="line">                <span class="keyword">return</span> awardPool;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//根据抽奖活动得分选择奖池</span></span><br><span class="line">    <span class="keyword">private</span> AwardPool <span class="title function_">chooseAwardPoolByScore</span><span class="params">(List&lt;AwardPool&gt; awardPools, <span class="type">int</span> gameScore)</span> &#123;...&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">package</span> com.company.team.bussiness.lottery.domain.valobj;</span><br><span class="line"><span class="keyword">import</span> ...;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AwardPool</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String cityIds;<span class="comment">//奖池支持的城市</span></span><br><span class="line">    <span class="keyword">private</span> String scores;<span class="comment">//奖池支持的得分</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;Awrad&gt; awards;<span class="comment">//奖池中包含的奖品</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//当前奖池是否与城市匹配</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">matchedCity</span><span class="params">(<span class="type">int</span> cityId)</span> &#123;...&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//当前奖池是否与用户得分匹配</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">matchedScore</span><span class="params">(<span class="type">int</span> score)</span> &#123;...&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//根据概率选择奖池</span></span><br><span class="line">    <span class="keyword">public</span> Award <span class="title function_">randomGetAward</span><span class="params">()</span> &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="改造"><a href="#改造" class="headerlink" title="改造"></a>改造</h3><p>不认可原文对DP&#x2F;VO、Entity、Aggregate的建模方式</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Identifier</span> <span class="keyword">extends</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 对实体和聚合根进行声明和抽象</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Entity</span>&lt;T <span class="keyword">extends</span> <span class="title class_">Identifier</span>&gt; &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">AggregateRoot</span>&lt;T <span class="keyword">extends</span> <span class="title class_">Identifier</span> &gt; <span class="keyword">extends</span> <span class="title class_">Entity</span>&lt;T&gt;&#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LotteryId</span> &#123;</span><br><span class="line">    <span class="keyword">final</span> <span class="type">int</span> id;</span><br><span class="line">    <span class="comment">// ... 其他可能的属性，比如业务方等</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 强制显式构造器</span></span><br><span class="line">    LotteryId(<span class="type">int</span> id) &#123;</span><br><span class="line">        <span class="built_in">this</span>.id = id;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DrawLottery</span> <span class="keyword">implements</span> <span class="title class_">AggregateRoot</span>&lt;LotteryId&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> LotteryId id;</span><br><span class="line">    <span class="comment">// 抽奖结果聚合在一起</span></span><br><span class="line">    <span class="keyword">private</span> SendResult result;</span><br><span class="line">    <span class="keyword">private</span> List&lt;AwardPool&gt; awardPools;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 屏蔽无参构造器，建议通过工厂来构建符合标准的entity instance</span></span><br><span class="line">    <span class="keyword">private</span> <span class="title function_">DrawLottery</span><span class="params">()</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> SendResult <span class="title function_">draw</span><span class="params">(DrawLotteryContext context)</span> &#123;</span><br><span class="line">        <span class="comment">// DrawLotteryContext是一个DP/VO，在构造时进行校验，使用时不用校验</span></span><br><span class="line">        <span class="type">AwardPool</span> <span class="variable">pool</span> <span class="operator">=</span> choosePool(context);</span><br><span class="line">        <span class="keyword">if</span> (pool == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125; </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SendResult</span>(pool.randomGetAward());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Pool <span class="title function_">choosePool</span><span class="params">(DrawLotteryContext context)</span> &#123;</span><br><span class="line">        <span class="comment">// 策略模式重构</span></span><br><span class="line">        <span class="type">PoolStrategy</span> <span class="variable">strategy</span> <span class="operator">=</span> PoolStrategyFactory.get(context);</span><br><span class="line">        <span class="keyword">for</span> (AwardPool pool: awardPools) &#123;</span><br><span class="line">            <span class="keyword">return</span> pool.match(strategy);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">package</span> com.company.team.bussiness.lottery.domain.valobj;</span><br><span class="line"><span class="keyword">import</span> ...;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Constraint</span> &#123;</span><br><span class="line">    <span class="keyword">final</span> <span class="keyword">private</span> List&lt;String&gt; cityIdList;<span class="comment">//奖池支持的城市</span></span><br><span class="line">    <span class="keyword">final</span> <span class="keyword">private</span> List&lt;String&gt; scoreList;<span class="comment">//奖池支持的得分</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AwardPool</span> <span class="keyword">implements</span> <span class="title class_">Entity</span>&lt;String&gt;&#123;</span><br><span class="line">    <span class="keyword">private</span> String id; <span class="comment">// 可以使用DP优化</span></span><br><span class="line">    <span class="keyword">private</span> Constraint constraint;</span><br><span class="line">    <span class="keyword">private</span> List&lt;Awrad&gt; awards;<span class="comment">//奖池中包含的奖品</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">match</span><span class="params">(PoolStrategy strategy)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> strategy.<span class="keyword">do</span>(constraint);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//根据概率选择奖池</span></span><br><span class="line">    <span class="keyword">public</span> Award <span class="title function_">randomGetAward</span><span class="params">()</span> &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>附加数据访问部分：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Repository</span>&lt;T <span class="keyword">extends</span> <span class="title class_">AggregateRoot</span>&lt;ID&gt;, ID <span class="keyword">extends</span> <span class="title class_">Identifier</span>&gt; &#123;</span><br><span class="line">    save(T t);</span><br><span class="line">    T <span class="title function_">find</span><span class="params">(ID id)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">DrawLotteryRepository</span> <span class="keyword">extends</span> <span class="title class_">Repository</span>&lt;DrawLottery, LotteryId&gt; &#123;</span><br><span class="line">    save(DrawLottery drawLottery);</span><br><span class="line">    DrawLottery <span class="title function_">find</span><span class="params">(LotteryId id)</span>;</span><br><span class="line">    <span class="comment">// 其他各种数据操作</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DrawLotteryRepositoryImpl</span> <span class="keyword">implements</span> <span class="title class_">DrawLotteryRepository</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> DrawLotteryDAO drawLotteryDAO;</span><br><span class="line">    <span class="keyword">private</span> SendResultDAO sendResultDAO;</span><br><span class="line">    <span class="keyword">private</span> DrawLotteryCache drawLotteryCache;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Transacitonal</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">save</span><span class="params">(DrawLottery drawLottery)</span> &#123;</span><br><span class="line">       <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p><img src="https://pic2.zhimg.com/80/v2-ed479babd573ffc3248123cb2d25c9f9_720w.webp" class="lazyload placeholder" data-srcset="https://pic2.zhimg.com/80/v2-ed479babd573ffc3248123cb2d25c9f9_720w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="img"></p>

      </div>
      <div class="post-tags-categories">
        
        <div class="tags">
          
            <a href="/my-blog/tags/%E6%9E%B6%E6%9E%84/" class="">
              架构
            </a>
          
        </div>
        
      </div>
      
        <div class="copyright">
  <ul class="post-copyright">
    <li class="post-copyright-author">
    <strong>作者:  </strong>老顾</a>
    </li>
    <li class="post-copyright-link">
    <strong>文章链接:  </strong>
    <a href="/my-blog/2023/06/29/DDD四层架构解析/" target="_blank" title="DDD四层架构解析">https://gitee.com/coderlaogu/2023/06/29/DDD四层架构解析/</a>
    </li>
    <li class="post-copyright-license">
      <strong>版权声明:   </strong>
      本网站所有文章除特别声明外,均采用 <a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" title="Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)">CC BY-NC-ND 4.0</a>
      许可协议。转载请注明出处!
    </li>
  </ul>
<div>
      
    </article>
    <!-- 上一篇文章和下一篇文章 -->
    
      <!-- 文章详情页的上一页和下一页 -->
<div class="post-nav">



  
  <div class="post-nav-prev post-nav-item">
    <div class="post-nav-img" style="background-size: cover; 
      background-position: center center;">
      <img class="lazyload lazyload placeholder" src="https://picx.zhimg.com/80/v2-b44931ea513e5f7b3b06f2703f79653c_1440w.webp" class="lazyload placeholder" data-srcset="https://picx.zhimg.com/80/v2-b44931ea513e5f7b3b06f2703f79653c_1440w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="">
    </div>
    <a href="/my-blog/2023/07/05/%E9%A1%B9%E7%9B%AE%E4%B8%AD%E5%BC%95%E5%85%A5%E7%AC%AC%E4%B8%89%E6%96%B9%E6%94%AF%E4%BB%98%E6%B5%81%E7%A8%8B/" class="post-nav-link">
      <div class="title">
        <i class="fas fa-angle-left"></i> 上一篇:
        <div class="title-text">项目中引入第三方支付流程</div>
      </div>
      
      <!-- <div class="content">
        对于支付过程涉及到三个角色：

第一个角色【前端】：用户手里的app&#x2F;用户手里的web页面进行支付
第二个角色
      </div> -->
    </a>
  </div>



  
  <div class="post-nav-next post-nav-item">
    <div class="post-nav-img" style="background-size: cover; 
      background-position: center center;">
      <img class="lazyload lazyload placeholder" src="https://pica.zhimg.com/v2-e466c5ffb776c583588566c4297de880_r.jpg" class="lazyload placeholder" data-srcset="https://pica.zhimg.com/v2-e466c5ffb776c583588566c4297de880_r.jpg" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" src="" alt="">
    </div>
    <a href="/my-blog/2023/06/27/2023%E5%B9%B4%E4%B8%8A%E5%8D%8A%E5%B9%B4%E5%81%9A%E7%9A%84%E4%BA%8B%E6%83%85/" class="post-nav-link">
      <div class="title">
        下一篇: <i class="fas fa-angle-right"></i>
        <div class="title-text">2023年上半年做的事情</div>
      </div>
      <!-- <div class="content">
        输入学完的专栏
陈皓-左耳听风
胡峰-程序员进阶攻略
白海飞-面试现场
罗剑锋-Kubernetes入门实战
周甲德-白
      </div> -->
    </a>
  </div>

</div>

    
    

    <!-- 打赏 -->
    
      <div id="appDonate" class="post-donate">
  <div id="donate_board" class="donate_bar center" ref="donate">
    <a id="btn_donate" class="btn_donate" href="javascript:;" title="打赏" @click="showDialogDrawer()"></a>
  </div>
  <transition name="fade">
    <div 
      class="donate-box-mask"
      v-cloak 
      v-show="visible"
      @click="cancelDialogDrawer()"
    >
    </div>
  </transition>
  <transition name="bounce">
    <div class="donate-box" v-cloak v-show="visible">
      <div class="donate-box_close">
        <i class="fas fa-times" aria-hidden="true" @click="cancelDialogDrawer" pointer></i>
      </div>
      <div class="donate-box_title">
        <h4>
          你的赏识是我前进的动力
        </h4>
      </div>
      <div class="donate-box_tab">
        <div class="Alipay" pointer :class="{'active': tabActive === 'Alipay'}" @click="changeTabActive('Alipay')">
          支付宝
        </div>
        <div class="WeChatpay" pointer :class="{'active': tabActive === 'WeChatpay'}" @click="changeTabActive('WeChatpay')">
          微信
        </div>
      </div>
      <div class="donate-box_img">
        <div class="AlipayImg" v-show="tabActive === 'Alipay'">
          <img src="https://pic1.zhimg.com/80/v2-79829ed763f645e036b1256fe8ff2138_1440w.webp" class="lazyload placeholder" data-srcset="https://pic1.zhimg.com/80/v2-79829ed763f645e036b1256fe8ff2138_1440w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="支付宝打赏" />
        </div> 
        <div class="WeChatpayImg" v-show="tabActive === 'WeChatpay'">
          <img src="https://pic4.zhimg.com/80/v2-bd6a4a089110697b1473add0e6006603_1440w.webp" class="lazyload placeholder" data-srcset="https://pic4.zhimg.com/80/v2-bd6a4a089110697b1473add0e6006603_1440w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="微信打赏" />
        </div>
      </div>
    </div>
  </transition>
</div>

<script>
  var body = document.body || document.documentElement || window;
  var vm = new Vue({
    el: '#appDonate',
    data: {
      visible: false,
      tabActive: 'Alipay',
      top: 0,
    },
    computed: {
    },
    mounted() {
    },
    methods: {
      showDialogDrawer() {
        this.visible = true;
        // 防止页面滚动，只能让弹框滚动
        // function getScroll() {
        //   return {
        //     left: window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0,
        //     top: window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
        //   };
        // }
        this.top = $(document).scrollTop() // or getScroll().top
        // console.log('aa', $('.main-content'));
        body.style.cssText = 'overflow: hidden;';
      },
      cancelDialogDrawer() {
        this.visible = false;
        body.removeAttribute('style');
        $(document).scrollTop(this.top)
      },
      changeTabActive(name) {
        this.tabActive = name;
      }
    },
    created() {}
  })
</script>
    

    <!-- 分享 -->
    
      <!-- https://github.com/overtrue/share.js -->
<!-- 文章详情页的分享 -->
<div class="social-share" data-sites="twitter,facebook,google,qq,qzone,wechat,weibo,douban,linkedin" data-wechat-qrcode-helper="<p>微信扫一扫即可分享！</p>"></div>

<script src="/my-blog/js/shareJs/social-share.min.js"></script>
</script>

<style>
  .social-share {
    margin: 20px 0;
  }
</style>


    
    
    <!-- 评论 -->
    <!-- 评论 -->

  <div id="myComment">
    
      
<section id="comments" style="padding: 1em;"
	class="animated bounceInUp">
	<div id="vcomment" class="comment"></div>
</section>
<style>
	#comments {
		background: rgba(255,255,255,0.9);
	}
	#veditor {
		background-image: url('https://img.zcool.cn/community/01a253594c71cfa8012193a329a77f.gif');
		background-size: contain;
		background-repeat: no-repeat;
		background-position: right;
		background-color: rgba(255, 255, 255, 0);
		resize: vertical;
	}
	#veditor:focus{
		background-position-y: 200px;
		transition: all 0.2s ease-in-out 0s;
	}
	#vcomment .vcards .vcard .vh .vhead .vtag.vvisitor {
		background-color: #42b983;
	}
	.v[data-class=v] .vbtn:active, .v[data-class=v] .vbtn:hover {
		color: #42b983;
		border-color: #42b983;
	}
	#vcomment .vcards .vcard .vhead .vsys i {
		display: none;
	}
	/* 底部valine链接 */
	#vcomment .vpower {
		display: none;
	}
	
	/* 底下注释是修改 名称和邮箱和网址输入框的样式 */
	/* #vcomment .vheader {
		display: flex;
		justify-content: space-around;
	}
	
	#vcomment .vheader .vnick {
		width: 31%;
		border: 2px solid #dedede;
		padding-left: 10px;
		padding-right: 10px;
		border-radius: 5px
	}

	#vcomment .vheader .vmail {
		width: 31%;
		border: 2px solid #dedede;
		padding-left: 10px;
		padding-right: 10px;
		border-radius: 5px
	}

	#vcomment .vheader .vlink {
		width: 31%;
		border: 2px solid #dedede;
		padding-left: 10px;
		padding-right: 10px;
		border-radius: 5px
	} */

	img.vimg {
		transition: all 1s;
		/* 头像旋转时间为 1s */
	}

	img.vimg:hover {
		transform: rotate(360deg);
		-webkit-transform: rotate(360deg);
		-moz-transform: rotate(360deg);
		-o-transform: rotate(360deg);
		-ms-transform: rotate(360deg);
	}

	#vcomment .vcards .vcard {
		padding: 15px 20px 0 20px;
		border-radius: 10px;
		margin-bottom: 15px;
		box-shadow: 0 0 4px 1px rgba(0, 0, 0, .12);
		transition: all .3s
	}

	#vcomment .vcards .vcard:hover {
		box-shadow: 0 0 8px 3px rgba(0, 0, 0, .12)
	}

	#vcomment .vcards .vcard .vh .vcard {
		border: none;
		box-shadow: none;
	}
</style>
    
  </div>

<!-- comment script in themes\hexo-theme-bamboo\layout\_partial\scripts\index.ejs -->


  </div>

  <!-- 目录 -->
  <aside id='l_side'>
  
    
      <section class="widget side_blogger">
  <div class='content'>
    
      
        <a class='avatar flat-box rectangle' href='/my-blog/about/'>
          <img src='https://pic1.zhimg.com/80/v2-51056addfd1a60b4d1de1546af250e88_1440w.webp'/>
        </a>
      
    
    
      <div class='text'>
        
          <h2>见自己，见天地，见众生，见道</h2>
        
        
        
          <p><span id="jinrishici-sentence">老顾的个人博客网站</span></p>
          <script src="https://sdk.jinrishici.com/v2/browser/jinrishici.js" charset="utf-8"></script>
        
      </div>
    
    
      <div class="social-wrapper">
        
          
            <a href="/my-blog/atom.xml"
              class="social fas fa-rss flat-btn"
              target="_blank"
              rel="external nofollow noopener noreferrer">
              
            </a>
          
        
          
            <a href="/my-blog/gu15328965713@gmail.com"
              class="social fas fa-envelope flat-btn"
              target="_blank"
              rel="external nofollow noopener noreferrer">
              
            </a>
          
        
          
            <a href="https://github.com/Engineer-Gu"
              class="social fab fa-github flat-btn"
              target="_blank"
              rel="external nofollow noopener noreferrer">
              
            </a>
          
        
          
            <a href="tencent://AddContact/?fromId=50&amp;fromSubId=1&amp;subcmd=all&amp;uin=1437594522"
              class="social fab fa-qq flat-btn"
              target="_blank"
              rel="external nofollow noopener noreferrer">
              
            </a>
          
        
      </div>
    
  </div>
</section>

    
  
  
  
    
  

  <div class="layout_sticky">    
    
      
<section class="widget side_toc">
  
  <header>
    
      <i style="color: " class="fas fa-list fa-fw" aria-hidden="true"></i><span class='name' style="color: ">本文目录</span>
    
  </header>


  <div class='content'>
    <div class="toc-main">
      <div class="toc-content">
        <!-- <ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#DP-x2F-VO"><span class="toc-text">DP&#x2F;VO</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E4%B8%9A%E5%8A%A1%E6%A1%88%E4%BE%8B"><span class="toc-text">一个简单的业务案例</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#DP%E5%88%9D%E4%BD%93%E9%AA%8C"><span class="toc-text">DP初体验</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%A4%BA%E4%BE%8B%E6%94%B9%E9%80%A0"><span class="toc-text">示例改造</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#DP%E4%BC%98%E5%8C%96"><span class="toc-text">DP优化</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#Entity%E3%80%81DomainService%E3%80%81Repository"><span class="toc-text">Entity、DomainService、Repository</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%80%9D%E8%80%83"><span class="toc-text">思考</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%9B%9E%E9%80%80%E5%88%B0%E6%97%A0Repository%E7%89%88%E6%9C%AC"><span class="toc-text">回退到无Repository版本</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%A2%9E%E5%8A%A0%E7%82%B9%E5%8A%9F%E8%83%BD"><span class="toc-text">增加点功能</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%9C%80%E6%B1%82%E6%A2%B3%E7%90%86"><span class="toc-text">需求梳理</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%B9%B3%E5%B9%B3%E6%97%A0%E5%A5%87%E7%9A%84%E5%86%99%E6%B3%95"><span class="toc-text">平平无奇的写法</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%8A%BD%E8%B1%A1RPC%E8%B0%83%E7%94%A8"><span class="toc-text">抽象RPC调用</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%8A%BD%E8%B1%A1%E6%95%B0%E6%8D%AE%E8%B0%83%E7%94%A8"><span class="toc-text">抽象数据调用</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%BB%BC%E5%90%88%E6%94%B9%E9%80%A0%E5%90%8E"><span class="toc-text">综合改造后</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E7%BB%9F%E4%B8%80%E8%AF%AD%E8%A8%80%E5%92%8C%E6%A8%A1%E5%9E%8B%E4%BB%B7%E5%80%BC"><span class="toc-text">统一语言和模型价值</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E8%81%9A%E5%90%88%E6%A0%B9-amp-%E9%99%90%E7%95%8C%E4%B8%8A%E4%B8%8B%E6%96%87"><span class="toc-text">聚合根&amp;限界上下文</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%81%9A%E5%90%88%E7%9A%84%E6%A6%82%E5%BF%B5"><span class="toc-text">聚合的概念</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#DP-x2F-VO%E3%80%81Entity%E7%9A%84%E5%A4%8D%E4%B9%A0"><span class="toc-text">DP&#x2F;VO、Entity的复习</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%AE%80%E5%8D%95%E7%9A%84%E8%BD%AC%E8%B4%A6%E9%9C%80%E6%B1%82"><span class="toc-text">简单的转账需求</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%81%9A%E5%90%88%E6%A0%B9%E5%88%9D%E4%BD%93%E9%AA%8C"><span class="toc-text">聚合根初体验</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%95%B0%E6%8D%AE%E8%AE%BF%E9%97%AE"><span class="toc-text">数据访问</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%99%90%E7%95%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E7%9A%84%E6%A6%82%E5%BF%B5"><span class="toc-text">限界上下文的概念</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#Exercise"><span class="toc-text">Exercise</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%8E%9F%E6%96%87%E5%86%99%E6%B3%95"><span class="toc-text">原文写法</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%94%B9%E9%80%A0"><span class="toc-text">改造</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%B0%8F%E7%BB%93"><span class="toc-text">小结</span></a></li></ol></li></ol> -->
        <div class="toc"></div>
      </div>
    </div>
  </div>
</section>
<!-- 手机端目录按钮 -->
<div id="toc-mobile-btn">
  <i class="fas fa-list-ul" aria-hidden="true"></i>
</div>

      
  <section class="widget side_recent_post">
    
  <header>
    
      <a style="color: " href='/tags/'><i class="fas fa-book fa-fw" aria-hidden="true"></i><span class='name'>最新文章</span></a>
    
  </header>


    <div class='content'>
      
      <!-- hash算法 -->
      
      <div class="aside-list">
        
          <div class="aside-list-item">
            
            
            

            <div class="post-img-box">
              <a href="/my-blog/2024/03/15/%E5%B0%86%E5%89%8D%E5%90%8E%E7%AB%AF%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2%E5%9C%A8%E6%9C%AC%E5%9C%B0%20Docker%20%E4%B8%8A/" class="post-img " style="background-size: cover; 
                background-position: center center;">
                <img class="lazyload lazyload placeholder" style="width:100%;height:100%;object-fit:cover;" data-src="https://pica.zhimg.com/v2-4aeccd941d89a30c21e31a4ea393f21a_r.jpg" class="lazyload placeholder" data-srcset="https://pica.zhimg.com/v2-4aeccd941d89a30c21e31a4ea393f21a_r.jpg" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="">
              </a>
            </div>
            <div class="post-date-title">
              <div>
                
                  <span class="post-date">03-15</span>
                
              </div>
              <a class="post-title" href="/my-blog/2024/03/15/%E5%B0%86%E5%89%8D%E5%90%8E%E7%AB%AF%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2%E5%9C%A8%E6%9C%AC%E5%9C%B0%20Docker%20%E4%B8%8A/">将前后端项目部署在本地 Docker 上</a>
            </div>
          </div>
        
          <div class="aside-list-item">
            
            
            

            <div class="post-img-box">
              <a href="/my-blog/2024/02/14/%E6%96%87%E4%BB%B6%E5%AF%BC%E5%87%BA%E4%B8%8B%E8%BD%BD%E9%9C%80%E6%B1%82%E5%89%8D%E5%90%8E%E7%AB%AF%E5%AE%9E%E7%8E%B0/" class="post-img " style="background-size: cover; 
                background-position: center center;">
                <img class="lazyload lazyload placeholder" style="width:100%;height:100%;object-fit:cover;" data-src="https://picx.zhimg.com/80/v2-b44931ea513e5f7b3b06f2703f79653c_1440w.webp" class="lazyload placeholder" data-srcset="https://picx.zhimg.com/80/v2-b44931ea513e5f7b3b06f2703f79653c_1440w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="">
              </a>
            </div>
            <div class="post-date-title">
              <div>
                
                  <span class="post-date">02-14</span>
                
              </div>
              <a class="post-title" href="/my-blog/2024/02/14/%E6%96%87%E4%BB%B6%E5%AF%BC%E5%87%BA%E4%B8%8B%E8%BD%BD%E9%9C%80%E6%B1%82%E5%89%8D%E5%90%8E%E7%AB%AF%E5%AE%9E%E7%8E%B0/">文件导出下载需求前后端实现</a>
            </div>
          </div>
        
          <div class="aside-list-item">
            
            
            

            <div class="post-img-box">
              <a href="/my-blog/2024/01/30/%E5%9F%BA%E4%BA%8EMinIO%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E4%B8%8B%E8%BD%BD/" class="post-img " style="background-size: cover; 
                background-position: center center;">
                <img class="lazyload lazyload placeholder" style="width:100%;height:100%;object-fit:cover;" data-src="https://pic4.zhimg.com/80/v2-83ea273b88e119d9615aed0d89aad4ab_1440w.webp" class="lazyload placeholder" data-srcset="https://pic4.zhimg.com/80/v2-83ea273b88e119d9615aed0d89aad4ab_1440w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="">
              </a>
            </div>
            <div class="post-date-title">
              <div>
                
                  <span class="post-date">01-30</span>
                
              </div>
              <a class="post-title" href="/my-blog/2024/01/30/%E5%9F%BA%E4%BA%8EMinIO%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E4%B8%8B%E8%BD%BD/">基于MinIO实现文件上传下载</a>
            </div>
          </div>
        
          <div class="aside-list-item">
            
            
            

            <div class="post-img-box">
              <a href="/my-blog/2023/12/24/%E3%80%8A%E5%A4%A7%E5%9E%8B%E7%BD%91%E7%AB%99%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/" class="post-img " style="background-size: cover; 
                background-position: center center;">
                <img class="lazyload lazyload placeholder" style="width:100%;height:100%;object-fit:cover;" data-src="https://pica.zhimg.com/v2-e466c5ffb776c583588566c4297de880_r.jpg" class="lazyload placeholder" data-srcset="https://pica.zhimg.com/v2-e466c5ffb776c583588566c4297de880_r.jpg" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="">
              </a>
            </div>
            <div class="post-date-title">
              <div>
                
                  <span class="post-date">12-24</span>
                
              </div>
              <a class="post-title" href="/my-blog/2023/12/24/%E3%80%8A%E5%A4%A7%E5%9E%8B%E7%BD%91%E7%AB%99%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/">《大型网站技术架构》读书笔记</a>
            </div>
          </div>
        
          <div class="aside-list-item">
            
            
            

            <div class="post-img-box">
              <a href="/my-blog/2023/11/06/%E3%80%8A%E7%A8%8B%E5%BA%8F%E5%91%98%E4%BF%AE%E7%82%BC%E4%B9%8B%E9%81%93-%E4%BB%8E%E5%B0%8F%E5%B7%A5%E5%88%B0%E4%B8%93%E5%AE%B6%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/" class="post-img " style="background-size: cover; 
                background-position: center center;">
                <img class="lazyload lazyload placeholder" style="width:100%;height:100%;object-fit:cover;" data-src="https://picx.zhimg.com/80/v2-85c31120acff76826ab53ea8934ef4bb_1440w.webp" class="lazyload placeholder" data-srcset="https://picx.zhimg.com/80/v2-85c31120acff76826ab53ea8934ef4bb_1440w.webp" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp" alt="">
              </a>
            </div>
            <div class="post-date-title">
              <div>
                
                  <span class="post-date">11-06</span>
                
              </div>
              <a class="post-title" href="/my-blog/2023/11/06/%E3%80%8A%E7%A8%8B%E5%BA%8F%E5%91%98%E4%BF%AE%E7%82%BC%E4%B9%8B%E9%81%93-%E4%BB%8E%E5%B0%8F%E5%B7%A5%E5%88%B0%E4%B8%93%E5%AE%B6%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/">《程序员修炼之道:从小工到专家》读书笔记</a>
            </div>
          </div>
        
      </div>
    </div>
  </section>

    
  </div>
</aside>

  <!-- 图片放大 Wrap images with fancybox support -->
  <script src="/my-blog/js/wrapImage.js"></script>
</div>

<!-- 文章详情页背景图 -->
<div id="appBgSwiper" style="position: fixed;left: 0;top: 0;width: 100%;height: 100%;z-index: -2;"
	:style="{'background-color': bgColor ? bgColor : 'transparent'}">
	<transition-group tag="ul" :name="names">
		<li v-for='(image,index) in img' :key='index' v-show="index === mark" class="bg-swiper-box">
			<img :src="image" class="bg-swiper-img no-lazy">
		</li>
	</transition-group>
</div>
<script>
	var vm = new Vue({
		el: '#appBgSwiper',
		data: {
			names: '' || 'fade' || 'fade', // translate-fade fade
			mark: 0,
			img: [],
			bgColor: '',
			time: null
		},
		methods: {   //添加方法
			change(i, m) {
				if (i > m) {
					// this.names = 'fade';
				} else if (i < m) {
					// this.names = 'fade';
				} else {
					return;
				}
				this.mark = i;
			},
			prev() {
				// this.names = 'fade';
				this.mark--;
				if (this.mark === -1) {
					this.mark = 3;
					return
				}
			},
			next() {
				// this.names = 'fade';
				this.mark++;
				if (this.mark === this.img.length) {
					this.mark = 0;
					return
				}
			},
			autoPlay() {
				// this.names = 'fade';
				this.mark++;
				if (this.mark === this.img.length) {
					this.mark = 0;
					return
				}
			},
			play() {
				let bgImgDelay = '' || '180000'
				let delay = parseInt(bgImgDelay) || 180000;
				this.time = setInterval(this.autoPlay, delay);
			},
			enter() {
				clearInterval(this.time);
			},
			leave() {
				this.play();
			}
		},
		created() {
			this.play()
		},
		beforeDestroy() {
			clearInterval(this.time);
		},
		mounted() {
			let prop = '' || '';
			let isImg = prop.includes('.bmp') || prop.includes('.jpg') || prop.includes('.png') || prop.includes('.tif') || prop.includes('.gif') || prop.includes('.pcx') || prop.includes('.tga') || prop.includes('.exif') || prop.includes('.fpx') || prop.includes('.psd') || prop.includes('.cdr') || prop.includes('.pcd') || prop.includes('.dxf') || prop.includes('.ufo') || prop.includes('.eps') || prop.includes('.ai') || prop.includes('.raw') || prop.includes('.WMF') || prop.includes('.webp') || prop.includes('.jpeg') || prop.includes('http://') || prop.includes('https://')
			if (isImg) {
				let img = prop.split(',');
				let configRoot = '/my-blog/'
				let arrImg = [];
				img.forEach(el => {
					var Expression = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
					var objExp = new RegExp(Expression);

					if (objExp.test(el)) {
						// http or https
						arrImg.push(el);
					} else {
						// 非http or https开头
						// 本地文件
						let firstStr = el.charAt(0);
						if (firstStr == '/') {
							el = el.substr(1); // 删除第一个字符 '/',因为 configRoot最后一个字符为 /
						}
						el = configRoot + el;
						arrImg.push(el);
					}
				})
				this.img = arrImg;
			} else {
				this.bgColor = prop;
			}
		}
	})
</script>

<style>
	.bg-swiper-box {
		position: absolute;
		display: block;
		width: 100%;
		height: 100%;
	}

	.bg-swiper-img {
		object-fit: cover;
		width: 100%;
		height: 100%;
	}
</style>




  <script>
  function loadMermaid() {
    if (document.getElementsByClassName('mermaid').length) {
      if (window.mermaidJsLoad) mermaid.init()
      else {
        loadScript('https://unpkg.com/mermaid/dist/mermaid.min.js').then(() => {
          window.mermaidJsLoad = true
          mermaid.initialize({
            theme: 'default',
          })
          if ('true') {
            mermaid.init();
          }
        })
      }
    }
  };
  document.addEventListener("DOMContentLoaded", function () {
    loadMermaid();
  })

  document.addEventListener('pjax:complete', function () {
    loadMermaid();
  })
  
</script>


      </main>
    </div>

    <!-- 页脚 -->
    
  
  
    <!-- 底部鱼儿跳动效果，依赖于jquery-->
<div id="j-fish-skip" style=" position: relative;height: 153px;width: auto;"></div>
<script>
  var RENDERER = {
    POINT_INTERVAL: 5,
    FISH_COUNT: 3,
    MAX_INTERVAL_COUNT: 50,
    INIT_HEIGHT_RATE: .5,
    THRESHOLD: 50,
    FISH_COLOR: '',
    init: function () {
      this.setFishColor(); this.setParameters(), this.reconstructMethods(), this.setup(), this.bindEvent(), this.render()
    },
    setFishColor: function () {
      let isDark = JSON.parse(localStorage.getItem('dark')) || JSON.parse('false');
      if (isDark) {
        this.FISH_COLOR = '#222'; // 暗黑色，有时间把这整成一个变量
      } else {
        this.FISH_COLOR = '' || 'rgba(66, 185, 133, 0.8)';
      }
    },
    setParameters: function () {
      this.$window = $(window), this.$container = $("#j-fish-skip"), this.$canvas = $("<canvas />"), this.context = this.$canvas.appendTo(this.$container).get(0).getContext("2d"), this.points = [], this.fishes = [], this.watchIds = []
    },
    createSurfacePoints: function () {
      var t = Math.round(this.width / this.POINT_INTERVAL);
      this.pointInterval = this.width / (t - 1), this.points.push(new SURFACE_POINT(this, 0));
      for (var i = 1; i < t; i++) {
        var e = new SURFACE_POINT(this, i * this.pointInterval),
          h = this.points[i - 1];
        e.setPreviousPoint(h), h.setNextPoint(e), this.points.push(e)
      }
    },
    reconstructMethods: function () {
      this.watchWindowSize = this.watchWindowSize.bind(this), this.jdugeToStopResize = this.jdugeToStopResize.bind(this), this.startEpicenter = this.startEpicenter.bind(this), this.moveEpicenter = this.moveEpicenter.bind(this), this.reverseVertical = this.reverseVertical.bind(this), this.render = this.render.bind(this)
    },
    setup: function () {
      this.points.length = 0, this.fishes.length = 0, this.watchIds.length = 0, this.intervalCount = this.MAX_INTERVAL_COUNT, this.width = this.$container.width(), this.height = this.$container.height(), this.fishCount = this.FISH_COUNT * this.width / 500 * this.height / 500, this.$canvas.attr({
        width: this.width,
        height: this.height
      }), this.reverse = !1, this.fishes.push(new FISH(this)), this.createSurfacePoints()
    },
    watchWindowSize: function () {
      this.clearTimer(), this.tmpWidth = this.$window.width(), this.tmpHeight = this.$window.height(), this.watchIds.push(setTimeout(this.jdugeToStopResize, this.WATCH_INTERVAL))
    },
    clearTimer: function () {
      for (; this.watchIds.length > 0;) clearTimeout(this.watchIds.pop())
    },
    jdugeToStopResize: function () {
      var t = this.$window.width(),
        i = this.$window.height(),
        e = t == this.tmpWidth && i == this.tmpHeight;
      this.tmpWidth = t, this.tmpHeight = i, e && this.setup()
    },
    bindEvent: function () {
      this.$window.on("resize", this.watchWindowSize), this.$container.on("mouseenter", this.startEpicenter), this.$container.on("mousemove", this.moveEpicenter)
    },
    getAxis: function (t) {
      var i = this.$container.offset();
      return {
        x: t.clientX - i.left + this.$window.scrollLeft(),
        y: t.clientY - i.top + this.$window.scrollTop()
      }
    },
    startEpicenter: function (t) {
      this.axis = this.getAxis(t)
    },
    moveEpicenter: function (t) {
      var i = this.getAxis(t);
      this.axis || (this.axis = i), this.generateEpicenter(i.x, i.y, i.y - this.axis.y), this.axis = i
    },
    generateEpicenter: function (t, i, e) {
      if (!(i < this.height / 2 - this.THRESHOLD || i > this.height / 2 + this.THRESHOLD)) {
        var h = Math.round(t / this.pointInterval);
        h < 0 || h >= this.points.length || this.points[h].interfere(i, e)
      }
    },
    reverseVertical: function () {
      this.reverse = !this.reverse;
      for (var t = 0, i = this.fishes.length; t < i; t++) this.fishes[t].reverseVertical()
    },
    controlStatus: function () {
      for (var t = 0, i = this.points.length; t < i; t++) this.points[t].updateSelf();
      for (t = 0, i = this.points.length; t < i; t++) this.points[t].updateNeighbors();
      this.fishes.length < this.fishCount && 0 == --this.intervalCount && (this.intervalCount = this.MAX_INTERVAL_COUNT, this.fishes.push(new FISH(this)))
    },
    render: function () {
      requestAnimationFrame(this.render), this.controlStatus(), this.context.clearRect(0, 0, this.width, this.height), this.context.fillStyle = this.FISH_COLOR;
      for (var t = 0, i = this.fishes.length; t < i; t++) this.fishes[t].render(this.context);
      this.context.save(), this.context.globalCompositeOperation = "xor", this.context.beginPath(), this.context.moveTo(0, this.reverse ? 0 : this.height);
      for (t = 0, i = this.points.length; t < i; t++) this.points[t].render(this.context);
      this.context.lineTo(this.width, this.reverse ? 0 : this.height), this.context.closePath(), this.context.fill(), this.context.restore()
    }
  },
  SURFACE_POINT = function (t, i) {
    this.renderer = t, this.x = i, this.init()
  };
  SURFACE_POINT.prototype = {
    SPRING_CONSTANT: .03,
    SPRING_FRICTION: .9,
    WAVE_SPREAD: .3,
    ACCELARATION_RATE: .01,
    init: function () {
      this.initHeight = this.renderer.height * this.renderer.INIT_HEIGHT_RATE, this.height = this.initHeight, this.fy = 0, this.force = {
        previous: 0,
        next: 0
      }
    },
    setPreviousPoint: function (t) {
      this.previous = t
    },
    setNextPoint: function (t) {
      this.next = t
    },
    interfere: function (t, i) {
      this.fy = this.renderer.height * this.ACCELARATION_RATE * (this.renderer.height - this.height - t >= 0 ? -1 : 1) * Math.abs(i)
    },
    updateSelf: function () {
      this.fy += this.SPRING_CONSTANT * (this.initHeight - this.height), this.fy *= this.SPRING_FRICTION, this.height += this.fy
    },
    updateNeighbors: function () {
      this.previous && (this.force.previous = this.WAVE_SPREAD * (this.height - this.previous.height)), this.next && (this.force.next = this.WAVE_SPREAD * (this.height - this.next.height))
    },
    render: function (t) {
      this.previous && (this.previous.height += this.force.previous, this.previous.fy += this.force.previous), this.next && (this.next.height += this.force.next, this.next.fy += this.force.next), t.lineTo(this.x, this.renderer.height - this.height)
    }
  };
  var FISH = function (t) {
    this.renderer = t, this.init()
  };
  FISH.prototype = {
    GRAVITY: .4,
    init: function () {
      this.direction = Math.random() < .5, this.x = this.direction ? this.renderer.width + this.renderer.THRESHOLD : -this.renderer.THRESHOLD, this.previousY = this.y, this.vx = this.getRandomValue(4, 10) * (this.direction ? -1 : 1), this.renderer.reverse ? (this.y = this.getRandomValue(1 * this.renderer.height / 10, 4 * this.renderer.height / 10), this.vy = this.getRandomValue(2, 5), this.ay = this.getRandomValue(.05, .2)) : (this.y = this.getRandomValue(6 * this.renderer.height / 10, 9 * this.renderer.height / 10), this.vy = this.getRandomValue(-5, -2), this.ay = this.getRandomValue(-.2, -.05)), this.isOut = !1, this.theta = 0, this.phi = 0
    },
    getRandomValue: function (t, i) {
      return t + (i - t) * Math.random()
    },
    reverseVertical: function () {
      this.isOut = !this.isOut, this.ay *= -1
    },
    controlStatus: function (t) {
      this.previousY = this.y, this.x += this.vx, this.y += this.vy, this.vy += this.ay, this.renderer.reverse ? this.y > this.renderer.height * this.renderer.INIT_HEIGHT_RATE ? (this.vy -= this.GRAVITY, this.isOut = !0) : (this.isOut && (this.ay = this.getRandomValue(.05, .2)), this.isOut = !1) : this.y < this.renderer.height * this.renderer.INIT_HEIGHT_RATE ? (this.vy += this.GRAVITY, this.isOut = !0) : (this.isOut && (this.ay = this.getRandomValue(-.2, -.05)), this.isOut = !1), this.isOut || (this.theta += Math.PI / 20, this.theta %= 2 * Math.PI, this.phi += Math.PI / 30, this.phi %= 2 * Math.PI), this.renderer.generateEpicenter(this.x + (this.direction ? -1 : 1) * this.renderer.THRESHOLD, this.y, this.y - this.previousY), (this.vx > 0 && this.x > this.renderer.width + this.renderer.THRESHOLD || this.vx < 0 && this.x < -this.renderer.THRESHOLD) && this.init()
    },
    render: function (t) {
      t.save(), t.translate(this.x, this.y), t.rotate(Math.PI + Math.atan2(this.vy, this.vx)), t.scale(1, this.direction ? 1 : -1), t.beginPath(), t.moveTo(-30, 0), t.bezierCurveTo(-20, 15, 15, 10, 40, 0), t.bezierCurveTo(15, -10, -20, -15, -30, 0), t.fill(), t.save(), t.translate(40, 0), t.scale(.9 + .2 * Math.sin(this.theta), 1), t.beginPath(), t.moveTo(0, 0), t.quadraticCurveTo(5, 10, 20, 8), t.quadraticCurveTo(12, 5, 10, 0), t.quadraticCurveTo(12, -5, 20, -8), t.quadraticCurveTo(5, -10, 0, 0), t.fill(), t.restore(), t.save(), t.translate(-3, 0), t.rotate((Math.PI / 3 + Math.PI / 10 * Math.sin(this.phi)) * (this.renderer.reverse ? -1 : 1)), t.beginPath(), this.renderer.reverse ? (t.moveTo(5, 0), t.bezierCurveTo(10, 10, 10, 30, 0, 40), t.bezierCurveTo(-12, 25, -8, 10, 0, 0)) : (t.moveTo(-5, 0), t.bezierCurveTo(-10, -10, -10, -30, 0, -40), t.bezierCurveTo(12, -25, 8, -10, 0, 0)), t.closePath(), t.fill(), t.restore(), t.restore(), this.controlStatus(t)
    }
  }, $(function () {
    RENDERER.init()
    $('.dark').click(function () {
      setTimeout(() => {
        RENDERER.setFishColor();
        RENDERER.context.fill();
      });
    })
  });
</script>
  
  <div class="footer bg-color">
    <div class="footer-main">
      
        
          <div class="link">
            
              
                <a href="/my-blog/atom.xml" class="social">
                  
                    <i class="fas fa-rss" aria-hidden="true"></i>
                  
                </a>
              
            
              
                <a href="mailto:1437594522@qq.com" class="social">
                  
                    <i class="fas fa-envelope" aria-hidden="true"></i>
                  
                </a>
              
            
              
                <a target="_blank" rel="noopener" href="https://github.com/Engineer-Gu" class="social">
                  
                    <i class="fab fa-github" aria-hidden="true"></i>
                  
                </a>
              
            
              
                <a href="tencent://AddContact/?fromId=50&amp;fromSubId=1&amp;subcmd=all&amp;uin=1437594522" class="social">
                  
                    <i class="fab fa-qq" aria-hidden="true"></i>
                  
                </a>
              
            
          </div>
        
      
        
          <div class="footer-copyright">
            <p>Copyright © 2019 - 2024 <a target="_blank" rel="noopener" href="https://github.com/Engineer-Gu">老顾</a> </p>

          </div>
        
      
    </div>
  </div>



    <!-- 渲染暗黑按钮 -->
    
      <div class="dark">
  <div class="dark-content">
    <i class="fas fa-moon" aria-hidden="true"></i>
    <!-- <span>关灯</span> -->
  </div>
  
</div>

<script>
  $(function() {
    let isDark = JSON.parse(localStorage.getItem('dark'))  || JSON.parse('false');
    if (isDark) {
      $(".dark-content").replaceWith(
          `
          <div class='dark-content'>
            <i class="fas fa-lightbulb" aria-hidden="true"></i>
          </div>
          `
        );
    }
    $('.dark').click(function() {
      if ($(document.body).is('.darkModel')) {
        $(document.body).removeClass('darkModel');
        localStorage.setItem('dark', false);
        $(".dark-content").replaceWith(
          `
          <div class='dark-content'>
            <i class="fas fa-moon" aria-hidden="true"></i>
          </div>
          `
        );
      } else {
        $(document.body).addClass('darkModel');
        localStorage.setItem('dark', true);
        $(".dark-content").replaceWith(
          `
          <div class='dark-content'>
            <i class="fas fa-lightbulb" aria-hidden="true"></i>
          </div>
          `
        );
      }
    })
  })
</script>
    
    <!-- 渲染回到顶部按钮 -->
    
      <div class="goTop top-btn-color" pointer>
  <i class="fas fa-arrow-up" aria-hidden="true"></i>
</div>
<script src="/my-blog/js/goTop.js"></script>

    
    <!-- 渲染左下角音乐播放器 -->
    
      <link rel="stylesheet" href="/my-blog/js/aplayer/APlayer@1.10.1.min.css">
<style>
.aplayer .aplayer-lrc p {
  
  display: none;
  
  font-size: 12px;
  font-weight: 700;
  line-height: 16px !important;
}

.aplayer .aplayer-lrc p.aplayer-lrc-current {
  
  display: none;
  
  font-size: 15px;
  color: #42b983;
}


.aplayer.aplayer-fixed.aplayer-narrow .aplayer-body {
  left: -66px !important;
}

.aplayer.aplayer-fixed.aplayer-narrow .aplayer-body:hover {
  left: 0px !important;
}


</style>
<meting-js  
  class=""
  server="tencent"
  type="playlist"
  id="8062553743"
  fixed='true'
  autoplay='false'
  theme='#42b983'
  loop='all'
  order='random'
  preload='auto'
  volume='0.7'
  list-folded='true'
>
</meting-js>

<!-- <style>
  #aplayer {
    position: fixed;
    left: 0;
    bottom: 300px;
  }
</style> -->
<script src="https://unpkg.com/aplayer@1.10.1/dist/APlayer.min.js"></script>
<script src="https://unpkg.com/meting@2/dist/Meting.min.js"></script>
    

    <!-- 图片放大 -->
    
      <script src="/my-blog/js/fancybox/jquery.fancybox.min.js"></script>
    

    <!-- 百度解析 -->
    <!-- Baidu Analytics -->

    <!-- Baidu Push -->

<script>
    (function () {
        var bp = document.createElement('script');
        var curProtocol = window.location.protocol.split(':')[0];
        if (curProtocol === 'https') {
            bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
        } else {
            bp.src = 'http://push.zhanzhang.baidu.com/push.js';
        }
        var s = document.getElementsByTagName("script")[0];
        s.parentNode.insertBefore(bp, s);
    })();
</script>

    
    <!-- 背景彩带 -->
    
      <script type="text/javascript" size="100" alpha='0.4' zIndex="-1" src="/my-blog/js/ribbon.min.js"></script>
    

    <script src="/my-blog/js/utils/index.js"></script>
    <script src="/my-blog/js/app.js"></script>
    
    <!-- 文章目录所需js -->
<!-- <link href="/my-blog/js/tocbot/tocbot.css" rel="stylesheet">
<script src="/my-blog/js/tocbot/tocbot.min.js"></script> -->

<script src="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.18.2/tocbot.min.js"></script>


<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.18.2/tocbot.css">

<script>
  var headerEl = 'h2, h3, h4',  //headers 
    content = '.post-detail',//文章容器
    idArr = {};  //标题数组以确定是否增加索引id
  //add #id
  var option = {
    // Where to render the table of contents.
    tocSelector: '.toc',
    // Where to grab the headings to build the table of contents.
    contentSelector: content,
    // Which headings to grab inside of the contentSelector element.
    headingSelector: headerEl,
    scrollSmooth: true,
    scrollSmoothOffset: -70,
    // headingsOffset: -($(window).height() * 0.4 - 45),
    headingsOffset: -($(window).height() * 0.4 - 70),
    // positionFixedSelector: '.toc-main',
    // positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto',
    activeLinkClass: 'is-active-link',
    orderedList: true,
    collapseDepth: 20,
    // onClick: function (e) {},
  }
  if ($('.toc').length > 0) {

    $(content).children(headerEl).each(function () {
      //去除空格以及多余标点
      var headerId = $(this).text().replace(/[\s|\~|`|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\_|\+|\=|\||\|\[|\]|\{|\}|\;|\:|\"|\'|\,|\<|\.|\>|\/|\?|\：|\，|\。]/g, '');

      headerId = headerId.toLowerCase();
      if (idArr[headerId]) {
        //id已经存在
        $(this).attr('id', headerId + '-' + idArr[headerId]);
        idArr[headerId]++;
      }
      else {
        //id未存在
        idArr[headerId] = 1;
        $(this).attr('id', headerId);
      }
    });

    document.addEventListener("DOMContentLoaded", function () {
      tocbot.init(option);
      mobileTocClick();
    });

  }

  window.tocScrollFn = function () {
    return bamboo.throttle(function () {
      findHeadPosition();
    }, 100)()
  }
  window.addEventListener('scroll', tocScrollFn);

  const findHeadPosition = function (top) {
    if ($('.toc-list').length <= 0) {
      return false;
    }
    setTimeout(() => {  // or DOMContentLoaded 
      autoScrollToc();
    }, 0);
  }

  const autoScrollToc = function () {
    const $activeItem = document.querySelector('.is-active-link');
    const $cardToc = document.querySelector('.toc-content');
    const activePosition = $activeItem.getBoundingClientRect().top
    const sidebarScrollTop = $cardToc.scrollTop
    if (activePosition > (document.documentElement.clientHeight - 100)) {
      $cardToc.scrollTop = sidebarScrollTop + 150
    }
    if (activePosition < 150) {
      $cardToc.scrollTop = sidebarScrollTop - 150
    }
  }

  document.addEventListener('pjax:send', function () {
    if ($('.toc').length) {
      tocbot.destroy();
    }
  });

  document.addEventListener('pjax:complete', function () {
    if ($('.toc').length) {
      tocbot.init(option);
      mobileTocClick();
    }
  });
  
  // 手机端toc按钮点击出现目录
  const mobileTocClick = function () {
    const $cardTocLayout = document.getElementsByClassName('side_toc')[0];
    const $cardToc = $cardTocLayout.getElementsByClassName('toc-content')[0];
    let right = '45px';
    if (window.innerWidth >= 551 && window.innerWidth <= 992) {
      right = '100px'
    }
    const mobileToc = {
      open: () => {
        $cardTocLayout.style.cssText = 'animation: toc-open .3s; opacity: 1; right: ' + right
      },

      close: () => {
        $cardTocLayout.style.animation = 'toc-close .2s'
        setTimeout(() => {
          $cardTocLayout.style.cssText = "opacity:''; animation: ''; right: ''"
        }, 100)
      }
    }
    document.getElementById('toc-mobile-btn').addEventListener('click', () => {
      if (window.getComputedStyle($cardTocLayout).getPropertyValue('opacity') === '0') mobileToc.open()
      else mobileToc.close()
    })

    $cardToc.addEventListener('click', (e) => {
      if (window.innerWidth < 992) { // 小于992px的时候
        mobileToc.close()
      }
    })
  }
</script>

<style>
  /* .is-position-fixed {
    position: sticky !important;
    top: 74px;
  }

  .toc-main ul {
    counter-reset: show-list;
  }

  .toc-main ul li::before {
    content: counter(item)".";
    display: block;
    position: absolute;
    left: 12px;
    top: 0;
  } */
</style>
 

<!-- 设置导航背景 -->
<script>
  let setHeaderClass = () => {
    const nav = $('#navHeader');
    const navTop = nav.outerHeight();
    const winTop = $(window).scrollTop();
    if(winTop > navTop) {
      nav.addClass('header-bg-color');
    }
    else {
      nav.removeClass('header-bg-color');
    }
  };

  let scrollCollect = () => {
    return bamboo.throttle(function (e) {
      setHeaderClass();
    }, 200)()
  }

  let initHeaderBg = () => {
    setHeaderClass();
  }

  setHeaderClass();
  window.addEventListener('scroll', scrollCollect);

  document.addEventListener('pjax:send', function () {
    window.removeEventListener('scroll', scrollCollect)
  })
  document.addEventListener('pjax:complete', function () {
    window.addEventListener('scroll', scrollCollect);
    setHeaderClass();
  })
</script> 

<!-- 渲染issues标签里的内容 -->
<script>
  function loadIssuesJS() {
    if ($(".post-detail").find(".issues-api").length == 0) {
      return;
    } 
    loadScript('/my-blog/js/issues/index.js');
  };
  $(function () {
    loadIssuesJS();
  });
  document.addEventListener('pjax:complete', function () {
    if (typeof IssuesAPI == "undefined") {
      loadIssuesJS();
    }
  })
</script>

<!-- 输入框打字特效 -->
<!-- 输入框打字特效 -->

  <script src="/my-blog/js/activate-power-mode.js"></script>
  <script>
    POWERMODE.colorful = true;  // 打开随机颜色特效
    POWERMODE.shake = false;    // 关闭输入框抖动
    document.body.addEventListener('input', POWERMODE);//监听打字事件
  </script>


<!-- markdown代码一键复制功能 -->

  <link rel="stylesheet" href="https://unpkg.com/v-plugs-ayu/lib/ayu.css">
  <script src="https://unpkg.com/v-plugs-ayu/lib/ayu.umd.min.js"></script>
  <script src="/my-blog/js/clipboard/clipboard.min.js"></script>
  <div id="appCopy">
  </div>
  <script data-pjax>
    var vm = new Vue({
      el: '#appCopy',
      data: {
      },
      computed: {
      },
      mounted() {
        const that = this;
        var copy = '复制';
        /* code */
        var initCopyCode = function () {
          var copyHtml = '';
          copyHtml += '<button class="btn-copy" data-clipboard-snippet="" style="position:absolute;top:0;right:0;z-index:1;">';
          copyHtml += '<i class="fas fa-copy"></i><span>' + copy + '</span>';
          copyHtml += '</button>';
          $(".post-detail pre").not('.gutter pre').wrap("<div class='codeBox' style='position:relative;width:100%;'></div>")
          $(".post-detail pre").not('.gutter pre').before(copyHtml);
          new ClipboardJS('.btn-copy', {
            target: function (trigger) {
              return trigger.nextElementSibling;
            }
          });
        }
        initCopyCode();
        $('.btn-copy').unbind('click').bind('click', function () {
          doSomething();
        })
        $(document).unbind('keypress').bind('keypress', function (e) {
          if (e.ctrlKey && e.keyCode == 67) {
            doSomething();
          }
        })

        function doSomething() {
          that.$notify({
            title: "成功",
            content: "代码已复制，请遵守相关授权协议。",
            type: 'success'
          })
        }
      },
      methods: {
      },
      created() { }
    })
  </script>
  

<!-- 图片懒加载 -->
<script defer src="https://unpkg.com/vanilla-lazyload@17.1.0/dist/lazyload.min.js"></script>
<script>
  // https://www.npmjs.com/package/vanilla-lazyload
  // Set the options globally
  // to make LazyLoad self-initialize
  window.lazyLoadOptions = {
    elements_selector: ".lazyload",
    threshold: 0
  };
  // Listen to the initialization event
  // and get the instance of LazyLoad
  window.addEventListener(
    "LazyLoad::Initialized",
    function (event) {
      window.lazyLoadInstance = event.detail.instance;
    },
    false
  );
  document.addEventListener('DOMContentLoaded', function () {
    lazyLoadInstance.update();
  });
  document.addEventListener('pjax:complete', function () {
    lazyLoadInstance.update();
  });
</script>


<!-- 卡片滚动动画 -->
   

<!-- 评论所需js -->

  
    <!-- 弹幕所需的css和js -->
    
<!-- 具体js，请前往valine/script.ejs查看 -->

    <script>
  var requiredFields = 'nick,mail';
  requiredFields = requiredFields.split(',');
  comment_el = '.comment';
  let looperValine = null;
  load_valine = function () {
    if ($(comment_el).length) {
      var valine = new Valine({
        el: '#vcomment',
        path: window.location.pathname,
        notify: false,
        verify: false,
        app_id: "6BozzCZTlWMB1BNp9HE0yqtW-gzGzoHsz",
        app_key: "1BuoV4FJKI4t3NIo0sYHWMno",
        placeholder: "客官，说点什么吧",
        avatar: "https://valine.js.org/avatar.html",
        master: "f5ee1285421e234a1dc13a9ea05951a1",   //博主邮箱md5
        tagMeta: ["博主","小伙伴","访客"],     //标识字段名
        friends: "",
        metaPlaceholder: { "nick": "昵称/QQ号", "mail": "邮箱" },
        requiredFields: requiredFields,
        enableQQ: true,
      });
      function debounce(fn) {
        var timerID = null
        return function () {
          var arg = arguments[0]   //获取事件
          if (timerID) {
            clearTimeout(timerID)
          }
          timerID = setTimeout(function () {
            fn(arg)              //事件传入函数
          }, 200)
        }
      }
      //查询评论 valine.Q('*').limit(7) -- 查询所有，限制7条, 下面的的代码是查询当前页面
      var themeDanmu = eval('false');
      var themeLoop = eval('true');
      var themeLooperTime = '5000' || 5000;
      var speed = '40' || 20;
      var isBarrager = true;
      if (themeDanmu == true) {
        do_barrager();
        if ($('.danmuBox').length <= 0) {
          $('.navbar').append('<div class="danmuBox"><div class="danmuBtn open"><span class="danmuCircle"></span><span class="danmuText">弹幕</span></div></div>');
        }
        $('.danmuBtn').on('click', debounce(
          function () {
            if ($('.danmuBtn').hasClass('open')) {
              $('.danmuBtn').removeClass('open')
              clearInterval(looperValine);
              $.fn.barrager.removeAll();
            } else {
              $('.danmuBtn').addClass("open");
              do_barrager();
            }
          }
        ))
      }
      function do_barrager() {
        isBarrager && valine.Q(window.location.pathname).find().then(function (comments) {
          // var num = 0; // 可以记录条数，循环调用的时候只取最新的评论放入弹幕中
          var run_once = true;
          var looper_time = themeLooperTime;
          var total = comments.length;
          // var looper = null;
          var index = 0;
          if (total > 0) {
            barrager();
          } else {
            // 当评论数为0的时候，自动关闭弹幕
            // $('.danmuBtn').removeClass('open');
          }
          function barrager() {
            if (run_once) {
              //如果是首次执行,则设置一个定时器,并且把首次执行置为false
              looperValine = setInterval(barrager, looper_time);
              run_once = false;
            }
            var content = comments[index]._serverData.comment;
            var email = comments[index]._serverData.mail;
            var link = comments[index]._serverData.link;
            var newcontent = content.substring(0, 50).replace(/<[^>]+>/g, "");
            //发布一个弹幕
            const item = {
              img: `https://q1.qlogo.cn/g?b=qq&nk=${email}&s=640`, //图片 
              info: newcontent, //文字 
              href: link, //链接 
              close: true, //显示关闭按钮 
              speed: speed, //延迟,单位秒,默认6 
              color: '#fff', //颜色,默认白色 
              old_ie_color: '#000000', //ie低版兼容色,不能与网页背景相同,默认黑色
            }
            $('body').barrager(item);
            //索引自增
            index++;
            //所有弹幕发布完毕，清除计时器。
            if (index == total) {
              clearInterval(looperValine);
              if (themeLoop === true) {
                setTimeout(function () {
                  do_barrager();
                }, 5000);
              } else {
                $('.danmuBtn').removeClass('open');
              }
              return false;
            }

          }
        })
      }
    }
  };
  $(document).ready(load_valine);
  document.addEventListener('pjax:send', function (e) {
    
  })
  document.addEventListener('pjax:complete', function () {
    load_valine();
  });
</script>

  


<!-- 鼠标点击特效 -->
<!-- 爱心点击 -->

  
    <script src="/my-blog/js/cursor/fireworks.js"></script>
  




  <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" data-pjax></script>


<!-- 轮播图标签 -->
<script>
  var bambooSwiperTag = {};
  function load_swiper() {
    if (!document.querySelectorAll(".post-swiper-container")[0]) return;
    loadCSS("https://unpkg.com/swiper@6/swiper-bundle.min.css")
    loadScript("https://unpkg.com/swiper@6/swiper-bundle.min.js").then(() => {
      pjax_swiper();
    });
  }

  load_swiper();

  function pjax_swiper() {
    bambooSwiperTag.swiper = new Swiper('.post-swiper-container', {
      slidesPerView: 'auto',
      spaceBetween: 8,
      centeredSlides: true,
      loop: true,
      autoplay: true ? {
        delay: 3000,
        stopOnLastSlide: false,
        disableOnInteraction: false,
      } : false,
      pagination: {
        el: '.swiper-pagination',
        clickable: true,
      },
      navigation: {
        nextEl: '.swiper-button-next',
        prevEl: '.swiper-button-prev',
      },
      on:{
        init: function(){
          swiperAnimateCache(this); //隐藏动画元素 
          swiperAnimate(this); //初始化完成开始动画
        }, 
        slideChangeTransitionEnd: function(){ 
          swiperAnimate(this); //每个slide切换结束时也运行当前slide动画
          //this.slides.eq(this.activeIndex).find('.ani').removeClass('ani'); 动画只展现一次，去除ani类名
        } 
      }
    });
  }

  document.addEventListener('pjax:complete', function () {
    if (!document.querySelectorAll(".post-swiper-container")[0]) return;
    if (typeof bambooSwiperTag.swiper === "undefined") {
      load_swiper();
    } else {
      pjax_swiper();
    }
  });
</script>
    <!-- pjax -->
    

<!-- pjax -->


  <script src="/my-blog/js/pjax@0.2.8/index.js"></script>
  
    <!-- 样式位于：source/css/_third-party/pjaxanimate.styl -->

<div class="pjax-animate">
  
    <div class="loading-circle"><div id="loader-circle"></div></div>
    <script>
      window.ShowLoading = function() {
        $(".loading-circle").css("display", "block");
      };
      window.HideLoading = function() {
        $(".loading-circle").css("display", "none");
      }
    </script>
  
	<script>
    document.addEventListener('pjax:complete', function () {
      window.HideLoading();
    })
    document.addEventListener('pjax:send', function () {
      window.ShowLoading();
    })
    document.addEventListener('pjax:error', function () {
      window.HideLoading();
    })
	</script>
</div>

  

  <script>
    var pjax = new Pjax({
      elements: 'a[href]:not([href^="#"]):not([href="javascript:void(0)"]):not([no-pjax])',   // 拦截正常带链接的 a 标签
      selectors: ["#pjax-container","title"],                                   // 根据实际需要确认重载区域
      cacheBust: false,   // url 地址追加时间戳，用以避免浏览器缓存
      timeout: 5000
    });

    document.addEventListener('pjax:send', function (e) {

      try {
        var currentUrl = window.location.pathname;
        var targetUrl = e.triggerElement.href;
        var banUrl = [""];
        if (banUrl[0] != "") {
          banUrl.forEach(item => {
            if(currentUrl.indexOf(item) != -1 || targetUrl.indexOf(item) != -1) {
              window.location.href = targetUrl;
            }
          });
        }
      } catch (error) {}

      $(window).unbind('resize');
      $(window).unbind('scroll');
      $(document).unbind('scroll');
      $(document).unbind('click');
      $('body').unbind('click');

    })
    
    document.addEventListener('pjax:complete', function () {
      $('script[data-pjax], .pjax-reload script').each(function () {
        $(this).parent().append($(this).remove());
      });
    });

    document.addEventListener('pjax:error', function (e) {
      window.location.href = e.triggerElement.href;
    })
    
    // 刷新不从顶部开始
    document.addEventListener("DOMContentLoaded", function () {
      history.scrollRestoration = 'auto';
    })
  </script>



  </body>
</html>